Compare commits

...

318 Commits

Author SHA1 Message Date
BarreiroT 6188e7aa7c return the clineenv api base url 2026-05-06 19:57:50 -03:00
BarreiroT 4f61954289 Fix race condition in the onboarding 2026-05-06 19:36:55 -03:00
BarreiroT 1c2a751382 Fix E2E tests 2026-05-06 19:20:51 -03:00
Tomás Barreiro f839c746e7 Dismiss onboarding after login (#10562)
* Dismiss onboarding after login

* add visual feedback when loading

* await the login
2026-05-06 22:48:07 +02:00
Bee b5e155039a chore: bump @clinebot/* packages to 0 0.0.38 (#10555)
* chore: bump @clinebot/* packages to 0 0.0.38

Upgrade @ai-sdk providers (amazon-bedrock, anthropic, gateway, google) and
@clinebot packages from v0.0.37 to v0.0.38. Adjust the return type of
_createSwitchToActModeTool() to AgentTool and update test imports to use
SdkSessionHost to match the updated core library.

* fix empty workspace

* remove resolveSdkWorkspaceRoot
2026-05-05 14:03:42 -07:00
Max Paulus 🥪 711046cf2d remove claude bias in other places 2026-05-04 12:31:13 -07:00
Max Paulus 🥪 f7210a4807 remove claude bias when reach mistake limit 2026-05-04 12:07:40 -07:00
Max Paulus 🥪 914527804c remove reference to restore session 2026-05-04 11:33:21 -07:00
Max Paulus 🥪 e37e91e4c5 remove references to sdk-session-factory 2026-05-04 11:33:21 -07:00
Max Paulus 🥪 823bfb1f60 fix subagent output 2026-05-04 11:33:21 -07:00
Max Paulus 🥪 41e80d236b add completion_result to last message when translating from sdk messages to cline messages 2026-05-04 11:33:21 -07:00
Max Paulus 🥪 f8cf469178 remove session-factory 2026-05-04 11:33:21 -07:00
Max Paulus 🥪 240a089e5d fix delete all and export buttons 2026-05-04 11:33:21 -07:00
Max Paulus 🥪 73c0881024 add toggle task favorite functionality 2026-05-04 11:33:21 -07:00
Max Paulus 🥪 c9206d90f1 rename sessionManager to sdkHost 2026-05-04 11:33:20 -07:00
Max Paulus 🥪 3fe42ab80d fix enable thinking toggle saving wrong model 2026-05-04 11:33:20 -07:00
Max Paulus 🥪 c5cb747bfa fix mcp auto approve settings 2026-05-04 11:33:20 -07:00
Max Paulus 🥪 4a7fb75dec update claude.md and .clinerules files
- remove stale info from .clinerules
2026-05-04 11:33:20 -07:00
Max Paulus 🥪 8ebc816489 add migration functionality so user can see old cline messages
- upon first loading an old conversation, we convert the format to new
sdk format and save it to sdk persistence layer
2026-05-04 11:33:20 -07:00
Dominic Cooney c108c5abe6 Remove Lazy Teammate Mode easter egg
Delete the April Fools' day feature including:
- Lazy teammate rules prompt (lazy-teammate-rules.ts)
- ClineLogoTired SVG component
- lazyTeammateModeEnabled setting from state keys, ExtensionMessage,
  controller state, settings update handler, webview context, and
  feature settings UI
- Proto fields marked as reserved (Settings #183, UpdateSettingsRequest #43)
2026-05-04 11:33:20 -07:00
Dominic Cooney f8ad3860c2 Set SHELL correctly in background shells. 2026-05-04 11:33:20 -07:00
Dominic Cooney ba342862a4 refactor: don't close terminals on profile switch, just key by effective shell
Instead of aggressively closing idle terminals and warning about busy
ones when the terminal profile changes, we now simply update the setting.
Existing terminals stay open and remain eligible for reuse if the user
switches back. New terminals use the new profile, and getOrCreateTerminal()
skips terminals with a different effective shell during reuse matching.

- Simplified setDefaultTerminalProfile() to void return
- Removed handleTerminalProfileChange() (dead code)
- Removed terminal close/warn notifications from updateSettings.ts
  and updateSettingsCli.ts
- Cleaned up unused imports
2026-05-04 11:33:19 -07:00
Dominic Cooney 9e640450c0 fix: compare effective shell paths for terminal reuse and profile switching
Previously, terminal matching compared raw shellPath values directly:
- 'default' profile terminals had shellPath=undefined
- 'zsh' profile terminals had shellPath='/bin/zsh'
These never matched, even though on macOS they resolve to the same shell.

Now effectiveShellPath() resolves undefined (default) to the actual
default shell via getShell(), so switching between 'zsh' and 'default'
on macOS won't needlessly close compatible terminals, and terminal reuse
correctly matches terminals running the same effective shell.
2026-05-04 11:33:19 -07:00
Dominic Cooney 6c8f9e4493 fix: set $SHELL env var to match selected terminal profile
When a non-default shell profile is selected (e.g. bash), the terminal
was spawning the correct shell binary via shellPath but inheriting
$SHELL from the parent process (VSCode extension host), which still
pointed to the user's login shell (e.g. /bin/zsh). This caused child
processes that read $SHELL (make, npm scripts, etc.) to see the wrong
shell.

Now VscodeTerminalRegistry.createTerminal() sets SHELL in the terminal's
env to match the shellPath when a specific profile is selected.
2026-05-04 11:33:19 -07:00
Dominic Cooney 42cdcc458d fix: propagate runtime terminal settings changes to live VscodeTerminalManager
When the user changes terminal settings (shell profile, timeout, reuse,
output limit) while terminals are open, the changes now propagate
immediately to the live VscodeTerminalManager instance via
controller.terminalManager (a public getter on SdkController).

Previously, updateSettings.ts and updateSettingsCli.ts tried to access
controller.task.terminalManager which doesn't exist in the SDK controller.
Now they use controller.terminalManager which is the shared instance
created lazily in SdkController.
2026-05-04 11:33:19 -07:00
Dominic Cooney 0aaf43d4cf feat: wire terminal settings (shell profile, timeout, reuse, output limit) into VscodeTerminalManager
When the lazy VscodeTerminalManager is first created, applyTerminalSettings()
reads from StateManager and configures:
- defaultTerminalProfile (shell choice: default/zsh/bash/etc.)
- shellIntegrationTimeout
- terminalReuseEnabled
- terminalOutputLineLimit

Also exposes a public terminalManager getter so updateSettings handlers
can apply runtime changes to the existing instance.
2026-05-04 11:33:19 -07:00
Dominic Cooney f51c8f6a68 feat: add custom run_commands tool with foreground/background terminal support
Introduces a VSCode-specific run_commands tool that replaces the SDK's
built-in version. This is an IDE-level feature built on top of the SDK,
not integrated into the SDK itself.

The tool supports two execution modes, switchable dynamically:
- Foreground (vscodeTerminal): Uses VscodeTerminalManager for visible
  VS Code terminals with shell integration, real-time output streaming,
  and 'Proceed While Running' support.
- Background (backgroundExec): Delegates to the SDK's createBashExecutor
  for headless child_process.spawn execution with configurable timeout.

Wiring:
- SdkController creates a lazy VscodeTerminalManager
- SdkSessionFactory passes getTerminalManager to VscodeSessionHost
- VscodeSessionHost suppresses SDK's built-in run_commands (bash: undefined)
- createVscodeExtraTools includes the custom run_commands tool

Design doc: sdk-migration/FOREGROUND-TERMINAL-DESIGN.md
2026-05-04 11:33:19 -07:00
Dominic Cooney 2176fbf1a5 fix(ENG-1885): deduplicate tool_result blocks on session resume with parallel tool calls
Root cause: initial-message-sanitizer only checked the immediately next
user message (i+1) for matching tool results. The SDK persists each
parallel tool result as a separate user message, so for N parallel tool
calls only 1/N was found and (N-1) placeholders were created. The
remaining (N-1) real results were left as separate messages. On merge,
this produced duplicate tool_result blocks causing Anthropic API errors.

Fix:
- Scan ALL consecutive user messages after the assistant to collect tool
  results, then consolidate into a single message via splice()
- Add defensive dedup in convertToOpenAiMessages as a safety net

Tests: 12 pass (5 sanitizer + 7 openai-format conversion)
2026-05-04 11:33:19 -07:00
Max Paulus 🥪 4c1706e6fa fix user message format 2026-05-04 11:33:19 -07:00
Max Paulus 🥪 9d72dc6661 fix tool rendering again 2026-05-04 11:33:18 -07:00
Max Paulus 🥪 78cdc55a02 remove lots of old task history code 2026-05-04 11:33:18 -07:00
Max Paulus 🥪 614a09807e remove acp from old cli and clineagent
- acp mode and clineagent is in sdk-wip now
- this will make it easier to clean up the sdkcontroller
2026-05-04 11:33:18 -07:00
Max Paulus 🥪 75ba27ea58 use sessionhost instead of other types 2026-05-04 11:33:18 -07:00
Max Paulus 🥪 0732118f89 fix perf issue 2026-05-04 11:33:18 -07:00
Max Paulus 🥪 1e36127ca3 fix clear task not actually updating the UI 2026-05-04 11:33:18 -07:00
Max Paulus 🥪 e1952e0698 use sdk for getTaskWithId 2026-05-04 11:33:18 -07:00
Max Paulus 🥪 9f3804ad67 listTasks uses sdk now 2026-05-04 11:33:17 -07:00
Mikołaj Kondratek 5294f2d8fd Update @clinebot modules to 0.0.37 2026-05-04 11:33:17 -07:00
Dominic Cooney 40a1e7e36e fix(ENG-1887): stuck "Thinking" after attempt_completion
The ask:"completion_result" message was emitted at content_end (when the
attempt_completion tool finished), but a usage event with
say:"api_req_started" always arrived between content_end and done,
becoming the last raw message. The webview uses the last raw message to
determine UI state, so it showed "Thinking..." instead of the completion
UI with the "Start New Task" button.

Fix: defer the ask:"completion_result" emission from content_end to the
done handler, which always runs after the usage event. The done handler
now unconditionally emits ask:"completion_result" (previously it was
conditional on !wasAttemptCompletionSeen). This ensures the ask is
always the final message regardless of whether attempt_completion was
used.
2026-05-04 11:33:17 -07:00
Dominic Cooney 7667780bc0 Add pending prompts to be compatible with SDK post cline/sdk-wip#263 2026-05-04 11:33:17 -07:00
Dominic Cooney bdcb733442 Implement preferredLanguage support. 2026-05-04 11:33:17 -07:00
Dominic Cooney b35def73fc fix: allow debug harness browser capture opt-out 2026-05-04 11:33:17 -07:00
Dominic Cooney 32ff768206 Delete a bunch of now-dead code. 2026-05-04 11:33:17 -07:00
Max Paulus 🥪 8f48efbe73 fixup! Remove Focus Chain from settings UI and state plumbing 2026-05-04 11:33:16 -07:00
Max Paulus 🥪 4c4f0079df feat: translate SDK spawn_agent events into rich subagent UI
The SDK spawn_agent tool was rendering as a generic tool row in the
webview. This translates its events into the ClineMessage types that
SubagentStatusRow already handles:

- content_start → say:"use_subagents" (prompts list with stable ts)
- content_update → say:"subagent" (running progress, partial=true)
- content_end → say:"subagent" (completed/failed) + say:"subagent_usage"

Also filters sub-agent agent_events by parentAgentId so only the root
agent produces ClineMessages. Without this, every sub-agent tool call,
text output, iteration, and usage event flooded the main chat.

No webview/CLI changes needed — existing SubagentStatusRow and
messageUtils filtering handle all the emitted message types.
2026-05-04 11:33:16 -07:00
Max Paulus 🥪 d30a4ae96e Remove strict plan mode setting
Remove the strictPlanModeEnabled feature toggle from the entire codebase.
The setting is not used by the SDK controller path and plan mode behavior
is now handled via system prompt instructions and the switch_to_act_mode
tool in the SDK layer.

Changes:
- Remove state key, ExtensionMessage field, and proto fields (reserved)
- Remove ToolExecutor plan-mode restriction logic (PLAN_MODE_RESTRICTED_TOOLS,
  isPlanModeToolRestricted, and the enforcement block)
- Remove from TaskConfig interface, validation, and TASK_CONFIG_KEYS
- Remove settings toggle from webview FeatureSettingsSection and CLI
- Remove from getStateToPostToWebview and updateSettings handlers
- Remove from ExtensionStateContext defaults and test mocks
- Regenerate proto types
2026-05-04 11:33:16 -07:00
Max Paulus 🥪 67699af03f Remove Focus Chain from settings UI and state plumbing
Remove the Focus Chain feature toggle from the settings view and
all associated state wiring:

- Settings UI: Remove Focus Chain toggle, reminder interval slider,
  and nested-key handling that was only used by focus chain
- ChatView: Remove focus chain checklist state, progress message
  memo, and placeholder memo
- TaskSection/TaskHeader: Remove focus chain props and FocusChain
  component rendering
- ExtensionStateContext: Remove focus chain settings default and
  currentFocusChainChecklist from state provider
- ExtensionMessage: Remove focusChainSettings and
  currentFocusChainChecklist from shared ExtensionState type
- getStateToPostToWebview: Stop sending focus chain state to webview
- updateSettings/updateSettingsCli: Remove focus chain settings
  update handlers and telemetry toggle tracking
2026-05-04 11:33:16 -07:00
Max Paulus 🥪 2672de7deb fix: read subagentsEnabled from StateManager so Settings toggle is respected
buildSessionConfig() only read subagentsEnabled from taskSettings (per-task
overrides), which is undefined for normal chat flow. This caused
enableSpawnAgent to always be false, making the SDK hide the spawn_agent
tool and causing the model to fall back to the skills tool.

Now reads the global subagentsEnabled setting from StateManager as the
default, with taskSettings still able to override it.
2026-05-04 11:33:16 -07:00
Max Paulus 🥪 f08b61323f add useBrowser auto approve back to support web fetch auto approve settings 2026-05-04 11:33:16 -07:00
Max Paulus 🥪 ae0b33ab2b remove browser use from cline
- we are removing browser use from cline as part of the migration to the
cline sdk. we may add it back later
2026-05-04 11:33:16 -07:00
Max Paulus 🥪 2054203791 fix: re-export AuthService from SDK so remote config gets auth tokens
The remote config system (fetch.ts, utils.ts) and other core modules
import AuthService from src/services/auth/AuthService.ts. In the SDK
migration, this was still the classic AuthService class with its own
separate singleton — completely disconnected from the SDK AuthService
that SdkController initializes with actual credentials.

This caused fetchRemoteConfig() to silently fail because:
- AuthService.getInstance().getAuthToken() returned null (no credentials)
- AuthService.getInstance().getActiveOrganizationId() returned null
- ClineAccountService.fetchUserRemoteConfig() failed silently

Fix: Replace src/services/auth/AuthService.ts with a re-export barrel
that points to src/sdk/auth-service.ts, matching the pattern used for
Controller (src/core/controller/index.ts → src/sdk/SdkController.ts).
This ensures all modules that import AuthService get the SDK singleton
which has the actual auth state.

Also stub out AuthServiceMock.ts which extended the classic AuthService
using protected members that no longer exist in the SDK version. The
mock was only used via dynamic require in E2E test mode and is not
referenced by any production code.
2026-05-04 11:33:16 -07:00
Max Paulus 🥪 9ccdc356bb feat: port remote config fetching to SdkController
The SdkController was missing all remote config support that the classic
Controller provides for enterprise customers (org-level policy enforcement,
MCP server management, provider lockdown, OpenTelemetry, etc.).

Changes:
- Add startRemoteConfigTimer() that fetches immediately then every 1 hour
- Chain timer start after auth restore in constructor
- Call fetchRemoteConfig() on login (handleAuthCallback)
- Fire-and-forget fetchRemoteConfig() at task start (initTask)
- Call clearRemoteConfig() on sign-out (handleSignOut)
- Clear interval timer in dispose() to prevent memory leaks
- Verify refreshRemoteConfig gRPC handler is already wired correctly
2026-05-04 11:33:15 -07:00
Max Paulus 🥪 c404a9d1d2 fix: handle bare array/string input in run_commands rendering (ENG-1867)
The SDK run_commands tool can pass input as a bare string[] (e.g.
["biome check --write src/"]) instead of wrapped { commands: [...] }.
parseToolInput() returns undefined for arrays, so commandText ended up
as "", rendering an empty shell fence in the command approval UI.

Extend both content_start and content_end handlers to detect bare
arrays and bare strings before falling through to parseToolInput(),
mirroring the fix already applied for search_codebase (S6-47).

Adds 6 unit tests covering bare array, multi-element array, bare
string, content_end round-trip, and wrapped-object regression.
2026-05-04 11:33:15 -07:00
Max Paulus 🥪 0bb8abb542 fix: implement exportTaskWithId and fix openDiskConversationHistory
- Replace exportTaskWithId stub in SdkController with real implementation
  that opens the task directory in the file manager (matching classic behavior)
- Fix openDiskConversationHistory to await openFileIntegration and move
  path construction inside the null check

Closes ENG-1828
2026-05-04 11:33:15 -07:00
Max Paulus 🥪 79b70f4652 fix: abort SDK session on mistake_limit_reached so UI buttons update correctly
After consecutive tool failures hitting maxConsecutiveMistakes (default 3),
the UI buttons stayed as Approve/Reject instead of updating to
"Proceed Anyways"/"Start New Task".

Root cause: In the SDK path, trackToolErrors() emitted the
mistake_limit_reached message but the SDK agent continued running,
immediately appending more messages. The mistake_limit_reached message
was never the last message, so the webview never showed correct buttons.

Fix: When the mistake limit is reached, set result.turnComplete = true
and abort the SDK session so the agent stops producing events. The
existing askResponse -> tryResumeSessionFromTask flow handles resumption
when the user clicks "Proceed Anyways".

Closes ENG-1874
2026-05-04 11:33:15 -07:00
Max Paulus 🥪 1cd9b75b09 fix: reuse timestamp for hook status messages to update in-place (ENG-1871) 2026-05-04 11:33:15 -07:00
Max Paulus 🥪 c5197a4f49 fix: emit mistake_limit_reached in SDK path after consecutive tool failures
The SDK execution path was missing consecutive tool error tracking that
the classic Task class provides via consecutiveMistakeCount. When tools
failed repeatedly, the UI buttons stayed showing "Approve"/"Reject"
instead of updating to "Proceed Anyways"/"Start New Task".

This change:
1. Adds toolError/toolSuccess flags to TranslationResult so the message
   translator signals when tool calls succeed or fail (content_end events
   with/without event.error)
2. Adds consecutiveToolErrorCount tracking to SdkSessionEventCoordinator
3. When the count reaches maxConsecutiveMistakes (default: 3), emits an
   ask="mistake_limit_reached" ClineMessage, which the webview already
   handles correctly to show the right buttons
4. Resets the counter on tool success or after emitting the limit message

Fixes ENG-1874
2026-05-04 11:33:15 -07:00
Max Paulus 🥪 6d02eacfc2 Fix SDK chat cost display for free Cline models 2026-05-04 11:33:15 -07:00
Max Paulus 🥪 1cb7cb8e76 bump sdk version 2026-05-04 11:33:14 -07:00
Max Paulus 🥪 f59768aea0 fix(sdk): poll feature flags during auth updates 2026-05-04 11:33:14 -07:00
Max Paulus 🥪 316a7f9f9e fix: sync OpenAI Codex OAuth credentials
Bridge SDK provider settings with the legacy Codex OAuth manager so ChatGPT Subscription sign-in updates settings state and inference can read the stored token. Clear both stores on sign-out and refresh incomplete SDK-stored credentials.
2026-05-04 11:33:14 -07:00
Max Paulus 🥪 157df2c482 break sdk controller down even further into smaller components 2026-05-04 11:33:14 -07:00
Max Paulus 🥪 ba7b7d984c split sdk controller even further
- made taskControl
2026-05-04 11:33:14 -07:00
Max Paulus 🥪 6719a28323 Extract SDK MCP and followup coordinators 2026-05-04 11:33:14 -07:00
Max Paulus 🥪 648b20667e Refactor SDK controller coordinators 2026-05-04 11:33:14 -07:00
cline 195554f034 fix(sdk): togglePlanActMode returns false to preserve pending input
The webview's onModeToggle handler in ChatTextArea.tsx treats the
returned boolean as 'did I consume your pending input' and calls
setInputValue('') when true. The SDK flow rebuilds the session
without consuming chatContent, so returning true incorrectly wiped
any text the user had typed before toggling.

Match the classic extension's semantic: only return true when the
chatContent was actually consumed as a plan-response message. The
SDK flow never does this, so both success branches now return false
(same-mode no-op already returned false).

ClineMessages and mode indicator continue to update correctly because:
- rebuildSessionForMode keeps this.task and its messageStateHandler
  alive across the session rebuild, so clineMessages are preserved
- getStateToPostToWebview() reads mode from stateManager which is
  updated before postStateToWebview() is called
- oldUnsubscribe() is called before old session stop/dispose, so no
  stale 'ended' events reach the gRPC bridge after rebuild

Adds 7 unit tests in src/sdk/toggle-plan-act-mode.test.ts covering
PLAN/ACT enum decode, chatContent pass-through, boolean round-trip,
invalid enum handling, and error propagation.
2026-05-04 11:33:13 -07:00
Max Paulus 🥪 354de542f0 fix(sdk): rebuild session with mode-specific provider/model on plan/act toggle
rebuildSessionForMode() now logs the resolved provider/model/apiKey from
buildSessionConfig({ cwd, mode: newMode }) so it is visible that the
mode-specific provider and model were picked up (planModeApiProvider /
actModeApiProvider, planModeApiModelId / actModeApiModelId, etc.).

Also adds an auth pre-check mirroring the one in initTask(): if the new
mode resolves to the cline provider without an auth token, emit the
standard auth error message sequence (say:task, say:api_req_started,
ask:api_req_failed) so the webview renders the "Sign in to Cline" button
via ErrorRow instead of crashing on the first SDK API call.

The pre-check runs BEFORE tearing down the old session, so the user
keeps their chat history and can retry after signing in or toggle back
to the original mode.
2026-05-04 11:33:13 -07:00
cline 23972fb4ce feat(sdk): rebuild session on plan/act mode toggle with cancel-style teardown
Replaces the old togglePlanActMode() behavior (which just cancelled the task and left the user to start over) with a full session rebuild that preserves conversation history while swapping in the new mode's system prompt and tools. Mirrors the CLI's onModeChange callback in apps/cli/src/runtime/run-interactive.ts.

Changes in src/sdk/SdkController.ts:

- Add rebuildSessionForMode(newMode): persists mode to global state, reads conversation history from the active session via loadInitialMessages(), tears down the old VscodeSessionHost (unsubscribe + stop + dispose), builds a fresh CoreSessionConfig for the new mode (new system prompt via buildSessionConfig, switch_to_act_mode re-injected for plan), preserves the task/session ID, and starts a new session with initialMessages. Task proxy stays alive so currentTaskItem and clineMessages remain stable across the rebuild.

- Implement applyPendingModeChange(): was a TODO stub; now reads pendingModeChange, clears it, and delegates to rebuildSessionForMode. This is the path the switch_to_act_mode tool uses when the model programmatically transitions plan -> act.

- Rewrite togglePlanActMode(): if activeSession exists, call rebuildSessionForMode; otherwise persist mode and refresh state. No more cancelTask() followed by the user manually restarting.

- Wire applyPendingModeChange into handleSessionEvent: when turnComplete/sessionEnded flips isRunning=false, check pendingModeChange and apply it fire-and-forget. Complements the existing check in fireAndForgetSend so we catch the mode change via either the event stream or the send promise resolution.

Cancel-style teardown on mid-turn toggle (matches classic Cline UX of "switch modes cancels the current task"):

- Reject any pendingToolApprovalResolve with { approved: false, reason: "Mode changed" } so the SDK tool executor unwinds cleanly

- Clear pendingAskResolve (no meaningful answer to give the dying session)

- Cancel the debounced save timer so it does not race with the finalization we write below

- await oldManager.abort(oldSessionId) (same call cancelTask uses) so the AbortSignal propagates through the tool executor: running shell commands get SIGTERM, in-flight LLM streams terminate

- Finalize in-memory messages via finalizeMessagesForSave() to strip partial: true flags and stamp the open api_req_started with cancelReason: "user_cancelled", re-add via messageStateHandler.addMessages() which updates by ts in-place, and persist synchronously via saveClineMessages so on-disk history reflects a cleanly cancelled turn

- Set activeSession.isRunning=false before teardown so late events from the dying session cannot flip state on the new session

Tests: npx tsc --noEmit and npx biome lint both pass clean.
2026-05-04 11:33:13 -07:00
Max Paulus 🥪 8dd91e697a feat(sdk): inject switch_to_act_mode tool in plan-mode sessions
Mirrors the CLI's plan -> act flow (apps/cli/src/runtime/run-interactive.ts)
by adding a programmatic mode-switch tool to SDK sessions started in plan
mode. When the model calls switch_to_act_mode after the user agrees to
the plan, the tool sets a pendingModeChange flag and returns a success
message so the current turn completes normally. After sessionManager.send()
returns, applyPendingModeChange() is invoked as the plumbing entry point
for the full session-rebuild flow (to be implemented in Task 4).

Changes to src/sdk/SdkController.ts:
- Import createTool and Tool type from @clinebot/shared
- Add pendingModeChange: Mode | null field
- Add createSwitchToActModeTool() private method matching the CLI's
  tool definition (name, description, success message, timeouts)
- Add injectModeExtraTools() helper and wire it into all four
  session-creation code paths: initTask, reinitExistingTaskFromId,
  resumeSessionFromTask, and restartSessionForMcpTools
- Add applyPendingModeChange() stub that reads and clears the flag
  (Task 4 will fill in the full session rebuild)
- Invoke applyPendingModeChange() from fireAndForgetSend's .then()
  after the turn completes (skipped for queue/steer deliveries)

CoreSessionConfig.mode is already set correctly in buildSessionConfig(),
so the SDK's plan preset (which disables editor tools) continues to be
selected -- our injected tool is merged with that preset.

Verification: tsc --noEmit passes with 0 errors; SDK vitest suite
shows 143 passing tests (same as baseline).
2026-05-04 11:33:13 -07:00
Max Paulus 🥪 cdc11e818b feat(sdk): append plan-mode instructions to system prompt in VSCode
Mirrors the CLI plan-mode guardrails (apps/cli/src/runtime/prompt.ts)
so plan mode in VSCode tells the model to explore/analyze/plan and NOT
implement. Previously buildClineSystemPrompt did not emit these
instructions, so plan mode in VSCode had weaker guardrails than the CLI.
2026-05-04 11:33:13 -07:00
Max Paulus 🥪 4228755175 feat: wire UserPromptSubmit and TaskCancel hooks via SDK AgentExtension plugin
Implement the remaining two feasible Cline hooks as SDK AgentExtension
plugins, since AgentHooks lacks the right hook points for these:

- UserPromptSubmit → onInput: fires before the prompt enters the agent
  loop, supports cancel and contextModification
- TaskCancel → onSessionShutdown: fires only on user-initiated
  cancellation (reason === session_stop), fire-and-forget

Changes:
- hooks-adapter.ts: add buildHookExtensions() returning AgentExtension[]
  with one inline extension (cline-lifecycle-hooks) implementing onInput
  and onSessionShutdown callbacks
- SdkController.ts: add buildExtensionsWithEmitter() method and wire
  config.extensions at all 4 session creation sites (initTask,
  reinitExistingTaskFromId, resumeSessionFromTask,
  restartSessionForMcpTools)

The existing buildAgentHooks() for the 4 AgentHooks-based hooks
(TaskStart, PreToolUse, PostToolUse, TaskComplete) is untouched.
2026-05-04 11:33:13 -07:00
Max Paulus 🥪 19822719ad fix: show 'Sign in to Cline' button instead of raw SDK error when not logged in
When using the 'cline' provider without authentication, the SDK throws a
generic 'Missing API key' error that surfaces as a raw red error message
with an infinite 'Thinking...' spinner. The classic extension shows a
friendly login prompt with a 'Sign in to Cline' button instead.

Fix by adding a pre-check in initTask() that detects the cline provider
with no auth token and emits the same message sequence the classic
extension uses (say:task -> say:api_req_started -> ask:api_req_failed
with a serialized ClineError). The webview's ErrorRow already handles
this pattern and renders the sign-in UI.

Also updates catch blocks in fireAndForgetSend(), askResponse(), and
reinitExistingTaskFromId() to detect cline auth errors and emit the
proper auth UI instead of raw say:error messages.
2026-05-04 11:33:13 -07:00
Max Paulus 🥪 b02b240331 feat: emit hook_status ClineMessages from hooks-adapter for chatview visibility
The SDK invokes AgentHooks callbacks inline (not through onEvent), so the
message-translator case "hook" handler never fires for adapter hooks. This
means hooks run silently with no UI feedback.

Fix by emitting hook_status ClineMessages directly from the hooks-adapter
callbacks via a HookMessageEmitter callback provided by SdkController.

Changes to hooks-adapter.ts:
- Add HookMessageEmitter type and buildHookStatusMessage() helper
- Add optional emitHookMessage param to buildAgentHooks()
- In all 4 callbacks, check factory.hasHook() before emitting
- Emit running/completed/cancelled/failed status messages

Changes to SdkController.ts:
- Add buildHooksWithEmitter() that wires emitter to messageStateHandler,
  pushMessageToWebview, and debouncedSaveClineMessages
- Override config.hooks at all 4 buildSessionConfig() call sites
2026-05-04 11:33:12 -07:00
Max Paulus 🥪 92646795e1 feat(sdk): bridge Cline file-based hooks into SDK AgentHooks interface
Create src/sdk/hooks-adapter.ts that maps 4 Cline hooks to SDK
lifecycle callbacks:
- TaskStart → onSessionStart
- PreToolUse → onToolCallStart
- PostToolUse → onToolCallEnd
- TaskComplete → onRunEnd (gated on finishReason === completed)

Each callback dynamically checks hooksEnabled via StateManager so
toggling mid-session takes effect immediately. All callbacks are
fail-open (errors logged, never block the SDK).

Wire buildAgentHooks() into buildSessionConfig() in
cline-session-factory.ts so every new session gets hook callbacks.
2026-05-04 11:33:12 -07:00
Max Paulus 🥪 2b90bc8239 fix(sdk): execute attempt_completion command parameter instead of ignoring it
The attempt_completion extra tool defined a command parameter in its schema
but the execute function silently discarded it, wasting tokens.

Re-use the SDK built-in bash executor (via createDefaultExecutors) to run
the command when provided, and append its output to the completion result
returned to the model.

Changes:
- vscode-runtime-builder.ts: createAttemptCompletionTool now accepts cwd,
  lazily creates a bash executor, and executes the command parameter
- vscode-session-host.ts: passes input.config.cwd to createVscodeExtraTools

Closes S6-47
2026-05-04 11:33:12 -07:00
Max Paulus 🥪 b9eaa43bb5 fix(sdk): parse mcp tool calls
- mcp tool calls weren't properly translated to the right format for
viewing in the chatview. this fixes that
2026-05-04 11:33:12 -07:00
Max Paulus 🥪 6854c80f2d fix(sdk): use delivery: "queue" for follow-up messages during active turns (S6-26C)
When the user sends a follow-up message while the agent is mid-turn,
askResponse() now detects isRunning and passes delivery: "queue" to
the SDK send() call. The SDK enqueues the message and drains it after
the current turn completes, preventing "already in progress" errors.

Changes:
- fireAndForgetSend(): accept optional delivery param, skip isRunning
  reset when message was queued (turn didnt complete)
- askResponse(): capture wasAlreadyRunning before setting isRunning,
  compute delivery accordingly, skip translator reset for queued msgs
- handleSessionEvent(): log pending_prompts/pending_prompt_submitted
  events for visibility
2026-05-04 11:33:12 -07:00
Max Paulus 🥪 16c5d966b6 fix(sdk): include MCP tools in toolPolicies for approval enforcement
MCP tools were bypassing the approval flow because buildToolPolicies()
only covered built-in SDK tools. MCP tools registered as extra tools
with serverName__toolName names had no policy entries, so the SDK
defaulted them to autoApprove:true.

- Add MCP tool iteration in buildToolPolicies() using McpHub.getServers()
- Gate MCP tool auto-approve on both the global useMcp toggle and each
  tool's individual autoApprove flag
- Use serverName__toolName format matching the SDK's default name transform
- Remove unnecessary regex sanitization helper (sdkMcpToolName)
2026-05-04 11:33:12 -07:00
Max Paulus 🥪 481a5e5304 fix(sdk): translate auto-approval settings into SDK toolPolicies
The SDK defaults all tools to autoApprove:true when no toolPolicies
are provided. The user's auto-approval settings (readFiles, editFiles,
executeSafeCommands, etc.) were not being translated into SDK
toolPolicies, so requestToolApproval was never called.

- Add buildToolPolicies() that maps AutoApprovalSettings actions to
  SDK tool names with { autoApprove: boolean } policies
- Add toolPolicies option to VscodeSessionHostOptions, pass through
  to ClineCore.create()
- Read autoApprovalSettings from StateManager in startNewSession()
  and pass the derived toolPolicies to VscodeSessionHost.create()
2026-05-04 11:33:12 -07:00
Max Paulus 🥪 6390aa7ed2 feat(sdk): wire requestToolApproval callback for non-auto-approved tools
Implement the requestToolApproval callback so the SDK can request user
approval for non-auto-approved tools (S6-26 Part A).

- Export sdkToolToClineSayTool from message-translator.ts for reuse
- Add handleRequestToolApproval() private method to SdkController that
  converts SDK ToolApprovalRequest to ClineSayTool JSON, emits a
  ClineMessage with type:ask/ask:tool, and returns a Promise resolved
  when the user clicks Approve/Reject in the webview
- Add handleAskQuestion() private method (extracted from inline callback)
- Wire both callbacks into startNewSession() via VscodeSessionHost.create()
- Add pendingToolApprovalResolve field to SdkController, resolved in
  askResponse() by reading taskState.askResponse (yesButtonClicked=approve,
  noButtonClicked=deny)
- Clean up pendingToolApprovalResolve in cancelTask() and clearTask()
  to prevent Promise leaks
- Update PROBLEMS.md to mark Part A as fixed
2026-05-04 11:33:12 -07:00
Max Paulus 🥪 21d545e70a update package-lock 2026-05-04 11:33:11 -07:00
Max Paulus 🥪 df56a80d0c feat(S6-26B): wire SDK ask_question tool for follow-up questions
The SDK built-in ask_question tool was excluded from the agent tool list
because no askQuestion executor was provided. The agent could not ask
clarifying questions when encountering ambiguity.

Changes:
- vscode-session-host.ts: Add askQuestion option, pass defaultToolExecutors
  to ClineCore.create() so the SDK includes ask_question in the tool list
- SdkController.ts: Implement askQuestion executor that emits ClineMessage
  with ask:"followup" (reusing existing webview UI), stores a pending
  Promise resolver, and resolves it when askResponse() is called.
  cancelTask()/clearTask() clear pendingAskResolve to prevent leaks.
- message-translator.ts: Update comment to reflect new handling
- PROBLEMS.md: Mark S6-26 Part B as fixed with evidence
2026-05-04 11:33:11 -07:00
Max Paulus 🥪 71af35830c fix(S6-48): file edit diffs show deletions (red) not just additions (green)
The SDK editor tool provides old_text and new_text, but the message
translator stored raw new_text in content. DiffEditRow did not recognize
it as a diff format and fell through to a fallback treating every line
as an addition (all green, no red).

Three fixes:
1. message-translator.ts editor case: when both old_text and new_text
   are present, build a SEARCH/REPLACE diff in the format DiffEditRow
   expects.
2. message-translator.ts apply_patch case: also check the input field
   (SDK format) and populate both content and diff.
3. ChatRow.tsx: prefer tool.diff over tool.content when passing to
   DiffEditRow, and check for either in the guard condition.

7 new tests, 1 updated test. All 72 message-translator tests pass.
2026-05-04 11:33:11 -07:00
Max Paulus 🥪 a6057a4488 fix(S6-47): search tool group shows empty regex and "/" path
The SDK search_codebase tool input can be a bare array or string
(per SearchCodebaseUnionInputSchema), but parseToolInput() only
handled objects. This caused empty regex in the UI. Additionally,
the SDK has no path param for search, so the webview showed "/".

Changes:
- message-translator.ts: Handle bare array and string input formats
  for search_codebase in sdkToolToClineSayTool()
- ToolGroupRenderer.tsx: Show "codebase" instead of "/" when search
  path is empty; fix getActivityText to not require path for search
- RequestStartRow.tsx: Same empty-path fixes for consistency
- 8 new unit tests covering all SDK input formats and content_end
  preservation
2026-05-04 11:33:11 -07:00
Dominic Cooney 5e540269f9 fix: resolve @mentions in SDK path before sending to agent
The SDK migration removed the classic parseMentions() call that resolved
context mentions (@/file, @problems, @git-changes, @https://url, @hash)
into inline content. The SDK's own mention enricher only handles simple
@path mentions and fails with the webview's @/path format.

Add resolveContextMentions() to SdkController that calls parseMentions()
before sending text to the SDK in all three send paths: initTask(),
askResponse(), and resumeSessionFromTask().

Fixes: Context command not returning any response in chat
2026-05-04 11:33:11 -07:00
Dominic Cooney 8733c056c5 fix: Resume Task button not displayed after cancellation
Three issues fixed in SdkController:

1. Race condition: Late-arriving SDK 'done' events after cancelTask()
   produced completion_result messages that replaced the 'Resume Task'
   button with 'Start New Task'. Added a filter in handleSessionEvent()
   that suppresses these messages when isRunning === false.

2. Dead session: After cancellation, activeSession still existed but the
   SDK session was dead (aborted). Clicking 'Resume Task' tried to send()
   to the dead session causing 'session not found' error. Changed
   askResponse() to also check isRunning, routing cancelled sessions to
   resumeSessionFromTask() which creates a fresh SDK session.

3. Idle resume: resumeSessionFromTask() only generated a [TASK RESUMPTION]
   prompt when there were no initialMessages. After cancellation there ARE
   initialMessages (conversation history), so the prompt was empty and the
   session sat idle. Changed to always send a resumption prompt when the
   user didn't type anything, matching classic extension behavior.
2026-05-04 11:33:11 -07:00
Dominic Cooney e04ddf631f Note tool impedance mistmatches in PROBLEMS. 2026-05-04 11:33:11 -07:00
Dominic Cooney 73c5528e44 Debug harness improvements for OAuth. 2026-05-04 11:33:10 -07:00
Dominic Cooney c4adbbffb6 rebase: remove vscodeTerminalExecutionMode override (main PR #10196)
Main's PR #10196 (Remove foreground terminal, commit 1862f1595) removed
the vscodeTerminalExecutionMode field from ExtensionState. The webview
now unconditionally renders commands as background-exec (ChatRow.tsx
hardcodes isBackgroundExec={true}), so the SDK-adapter override in
getStateToPostToWebview() is both a type error and dead code.

Removes the override and documents why it's no longer needed for any
future archaeology.
2026-05-04 11:33:10 -07:00
Max Paulus 🥪 d61fde394b style: lint-staged formatting fixes for S6-39/S6-40 commit 2026-05-04 11:33:10 -07:00
Max Paulus 🥪 ce5a255c0c fix: render URL and skill name in webFetch/useSkill tool calls (S6-39, S6-40)
S6-39: SDK fetch_web_content uses { requests: [{ url, prompt }] } but
sdkToolToClineSayTool() only checked for a top-level url field. Added
fallback to extract URL from requests[0].url.

S6-40: SDK skills tool uses { skill: "name" } but sdkToolToClineSayTool()
only checked skill_name and name fields. Added skill to the fallback chain.

7 new unit tests covering both SDK and classic input formats.
2026-05-04 11:33:10 -07:00
Max Paulus 🥪 f8ba599583 fix: suppress AbortError on task cancel and emit resume_task ask (S6-46, S6-34)
When the user cancels a running task, AbortController.abort() in the SDK
throws an AbortError that propagated unhandled to the VSCode developer
console. Three fixes:

1. VscodeSessionHost.abort(): wrap inner.abort() in try/catch that
   suppresses AbortError (expected) and re-throws others.

2. SdkController.cancelTask(): narrow try/catch to only the abort()
   call, suppress AbortError at debug level, and always proceed with
   cleanup. Emit ask: "resume_task" instead of say: "info" so the
   webview shows the Resume task button (fixes S6-34).

3. SdkController.fireAndForgetSend(): detect AbortError in .catch()
   and return early without emitting error events to the UI.
2026-05-04 11:33:10 -07:00
Max Paulus 🥪 0d39c35441 fix(S6-44): resolve RangeError: Invalid string length on task start
Two bugs combined to cause unbounded stdout accumulation in the SDK
file indexer when starting a new task:

1. SdkController used process.cwd() instead of getCwd() for workspace
   resolution. In VSCode extension host, process.cwd() returns the
   VSCode installation dir, not the workspace folder, causing rg to
   recurse enormous directory trees.

2. SDK file-indexer.ts rg command only excluded .git but not
   node_modules/dist/build/etc (which walkDir fallback did exclude).
   This is fixed in the linked SDK repo separately.

Fixes:
- Replace all process.cwd() calls in SdkController.ts and
  cline-session-factory.ts with getCwd() which resolves the actual
  workspace folder via HostProvider.workspace.getWorkspacePaths()
- Document fix in PROBLEMS.md as S6-44
2026-05-04 11:33:10 -07:00
Max Paulus 🥪 1fc12662c5 fix(sdk): format command output as raw text instead of JSON (S6-41)
The SDK run_commands tool returns ToolOperationResult[] with structured
output ({query, result, success, error?}). The message translator was
falling through to JSON.stringify() for non-string output, causing raw
JSON like [{"query":"ls","result":"...","success":true}] to appear in
the chat instead of formatted shell output.

Changes:
- Add extractToolOutputText() helper that extracts raw text from
  ToolOperationResult[] format, using result.result for success and
  result.error for failures
- Update command content_end handler to use extractToolOutputText()
- Override vscodeTerminalExecutionMode to backgroundExec in
  SdkController.getStateToPostToWebview() since SDK always uses
  background execution
- Add 15 unit tests covering all output extraction cases

Fixes S6-41
2026-05-04 11:33:10 -07:00
Max Paulus 🥪 13f5235f9a fix(S6-38): resolve workspace root via HostProvider instead of process.cwd()
The SdkController used process.cwd() in 4 places as the working directory
for SDK sessions. In VSCode, process.cwd() returns the extension host
directory, not the user workspace. This meant Cline could not find project
files without explicit paths.

Added SdkController.getWorkspaceRoot() which resolves the workspace root
via HostProvider.workspace.getWorkspacePaths() (delegates to
vscode.workspace.workspaceFolders[0].uri.fsPath), falling back to
process.cwd() only when no workspace folder is open.

Replaced all 4 process.cwd() calls in initTask(), reinitExistingTaskFromId(),
resumeSessionFromTask(), and restartSessionForMcpTools().

Also added a defensive warning log in buildSessionConfig() for the
process.cwd() fallback path.

Updated PROBLEMS.md: S4-3 marked fixed, S6-38 added.
2026-05-04 11:33:10 -07:00
Max Paulus 🥪 b15a2abf57 fix(S6-45): use transient prop to prevent isActive from leaking to DOM
Renamed isActive to \$isActive in StyledTabButton in ClineRulesToggleModal.tsx.
The dollar-sign prefix tells styled-components to consume the prop for
styling without forwarding it to the underlying DOM element, eliminating
the React warning "React does not recognize the isActive prop on a DOM element."

Updated PROBLEMS.md with S6-45 entry marked as verified fixed.
2026-05-04 11:33:09 -07:00
Dominic Cooney 3aef7df81b Bump ZOD version to unbreak JetBrains webview runtime bundling error. 2026-05-04 11:33:09 -07:00
Dominic Cooney 156f86f777 Update package-lock.json etc. 2026-05-04 11:33:09 -07:00
Dominic Cooney 811b489de4 Update to SDK 0.0.35. 2026-05-04 11:33:09 -07:00
Dominic Cooney 701cda0d52 Update PROBLEMS.md, cost display is fixed. 2026-05-04 11:33:09 -07:00
Bee 40ef83305a use latest sdk main (#10337)
* use latest sdk main

* update sdk auth service

* dont throw when scm not available
2026-05-04 11:33:09 -07:00
Max Paulus 🥪 000d237044 fix sdk initial messages 2026-05-04 11:33:09 -07:00
Max Paulus 🥪 2e6adc23f6 updated problems.md 2026-05-04 11:33:08 -07:00
Max Paulus 🥪 cd7781bf92 fix read_files tool call not showing all file paths. Fixed assistant message not appearing after tool call result 2026-05-04 11:33:08 -07:00
Max Paulus 🥪 5f0714ba1e cline chatview is able to show some tool calls 2026-05-04 11:33:08 -07:00
Max Paulus 🥪 67fd6fcffb fix(sdk): preserve task session id when reloading MCP tools
Keep the active task/session id stable during MCP tool-list restarts so currentTaskItem stays mapped and chat state is not lost after toggling MCP servers.
2026-05-04 11:33:08 -07:00
Max Paulus 🥪 abbe849786 refactor some sdk session code (DRY it up a bit) 2026-05-04 11:33:08 -07:00
Max Paulus 🥪 fb28011a96 fix (click new task while in mid task)
- fixes the issue where if I click new task while mid task, and navigate
back to old task, old task still shows as thinking
2026-05-04 11:33:08 -07:00
Max Paulus 🥪 2b929440ee resume session working
- new task button doesn't work thoguh
2026-05-04 11:33:08 -07:00
Max Paulus 🥪 dafd751dda todo session resume 2026-05-04 11:33:07 -07:00
Max Paulus 🥪 05855b3a6a handle hicap and requesty auth callback support 2026-05-04 11:33:07 -07:00
Max Paulus 🥪 d523af555d add openrouter auth callback support
- tested by choosing openrouter provider, clicking "get openrouter api
key", and then sending a prompt to openrouter (gpt-oss-120b:free model)
2026-05-04 11:33:07 -07:00
Max Paulus 🥪 4cb1bf1d1b fix mcp oauth callback
- tests: tested with remote notion mcp: https://mcp.notion.com/mcp
2026-05-04 11:33:07 -07:00
Dominic Cooney 5692670333 docs: add S6-35 — inference cost not displayed in task (minor) 2026-05-04 11:33:07 -07:00
Dominic Cooney 6392c1e0d4 docs: add S6-34 — cancel during generation doesn't show Resume task 2026-05-04 11:33:07 -07:00
Dominic Cooney 3d7e3a5451 docs: add S6-33 — insufficient credits shows raw error instead of buy-credits UI 2026-05-04 11:33:07 -07:00
Dominic Cooney f2b01b1c25 docs: add S6-32 — New Task button and delete disabled after MCP tool change 2026-05-04 11:33:07 -07:00
Dominic Cooney 434489cec0 docs: add S6-31 — conversation history lost after MCP tool changes 2026-05-04 11:33:06 -07:00
Dominic Cooney 2a2cafeeaf fix(sdk): update adapter for SDK sync (b2f9f62d → e99831a6)
SessionManager interface changes:
- Add update() and handleHookEvent() to VscodeSessionHost
- Remove readHooks() (no longer in SessionManager interface)
- Import HookEventPayload from @clinebot/core

Auth service fixes:
- Replace InstanceType<typeof UserInfo> with UserInfo type directly
- Fix null assignment to protobuf field (use empty create instead)
2026-05-04 11:33:06 -07:00
Dominic Cooney f99fe6e128 feat(sdk): add attempt_completion tool and fix duplicate green rectangles
In the SDK migration branch, every agent response was showing a green
'Task Completed' rectangle because the done event unconditionally emitted
completion_result messages. In the classic extension, the green rectangle
only appeared when the agent explicitly called the attempt_completion tool.

Changes:
- Register attempt_completion as a custom tool in VscodeRuntimeBuilder
  so the SDK agent can call it (the SDK has no built-in equivalent)
- Handle attempt_completion in MessageTranslator: content_start emits
  say:'completion_result' (green rectangle), content_end emits
  ask:'completion_result' with empty text (enables follow-up input
  without a second green rectangle)
- Track attempt_completion calls via MessageTranslatorState so the done
  event can skip emitting completion_result when already handled
- When attempt_completion is NOT called, done emits ask:'completion_result'
  with empty text (renders as InvisibleSpacer, no green rectangle)
- Update tests: 2 existing tests updated, 1 new test added for the
  suppression behavior (35/35 pass)
2026-05-04 11:33:06 -07:00
Dominic Cooney 107053fc84 docs: update PROBLEMS.md — S6-29 verified fixed, remove from priority list 2026-05-04 11:33:06 -07:00
Dominic Cooney efca727f5b fix(S6-29): emit completion_result after MCP tool reload
After restartSessionForMcpTools() completes, the webview was left in
a 'Thinking...' state because no ask:'completion_result' message was
emitted. The webview's handleSendMessage() requires clineAsk to be
set to enable follow-up input.

Fix: Emit ask:'completion_result' after the success info message so
the webview knows the agent is idle and enables the follow-up input.

Tested with debug harness:
1. Sent 'Say hello briefly' → completed
2. Toggled kamibiki MCP server off via UI
3. 'MCP tools changed' + 'reloaded successfully' messages appeared
4. Typed 'Say goodbye' → follow-up inference ran successfully
2026-05-04 11:33:06 -07:00
Dominic Cooney 9f673a2675 docs: update PROBLEMS.md — verify MCP tools, follow-up messages, debouncing
Mark as verified:
- S6-10: MCP tools work via VscodeRuntimeBuilder + McpHub bridge
- S6-14: VscodeRuntimeBuilder bridges all transport types
- S6-20: MCP tools available to agent (kb_status, kb_search tested)
- S6-28: MCP tool reload debouncing prevents duplicate messages
- S6-30: New — follow-up messages fixed (say→ask completion_result)
2026-05-04 11:33:06 -07:00
Dominic Cooney 92639018a7 fix: MCP tools + follow-up messages in SDK migration
Three fixes for the SDK migration:

1. Follow-up messages: Changed the 'done' event translation from
   say:'completion_result' to ask:'completion_result'. The webview's
   handleSendMessage() requires clineAsk to be set to send follow-up
   messages. Without the ask message, typing a follow-up and pressing
   Enter was silently dropped.

2. Session cleanup: Clear activeSession reference before stop/dispose
   to prevent re-entrant calls. Added 3s timeouts to stop()/dispose()
   to prevent UI blocking when sessions are in unexpected states.

3. MCP tool change debouncing: When an MCP server connects,
   notifyWebviewOfServerChanges() fires multiple times in quick
   succession. Added 300ms debounce with fingerprint quick-check
   to coalesce these into a single tool list change callback.

Tested with debug harness:
- MCP tools (kb_status, kb_search) work correctly
- Follow-up messages work via both gRPC and DOM (typing + Enter)
- MCP tools work in follow-up turns
2026-05-04 11:33:06 -07:00
Dominic Cooney d2a8e19dca feat: MCP tool list change detection and session restart
- McpHub: Added computeToolFingerprint(), setToolListChangeCallback(),
  clearToolListChangeCallback(), and checkToolListChanged() to detect
  when the set of available MCP tools changes (servers added/removed/
  reconnected). Only fires on actual tool list changes, not mere status
  updates.

- SdkController: Added handleMcpToolListChanged() which restarts the
  session immediately when idle, or defers via mcpToolRestartPending
  flag until the current turn completes. restartSessionForMcpTools()
  creates a new VscodeSessionHost with fresh tools, preserves
  conversation messages, and emits info messages to the chat.

- SdkController: Fixed MCP settings file path — was reading from
  VSCode extension storage (HostProvider.globalStorageFsPath/settings/)
  instead of ~/.cline/data/settings/ where the actual MCP settings live.

- task-proxy: Made taskId settable so session restart can update the
  proxy's session ID without recreating it.

- Tests: 16 unit tests for tool list change detection covering
  fingerprinting, callback firing, and edge cases.

Known issues: S6-28 (reload messages appear twice), S6-29 (new task
button broken after reload). See PROBLEMS.md.
2026-05-04 11:33:05 -07:00
Dominic Cooney cef09f37e6 fix(S6-27): restore conversation when opening task from history
The gRPC handler for showTaskWithId was calling controller.initTask()
which starts a NEW SDK session instead of loading the existing task's
messages from disk. Changed to call controller.showTaskWithId(id)
which correctly: (1) looks up the history item, (2) tears down any
active session, (3) creates a task proxy with loaded messages,
(4) pushes messages through both state updates and partial message
stream, (5) posts state to the webview.

Verified with debug harness: send message → new task → click history
item → conversation restored with all messages.
2026-05-04 11:33:05 -07:00
Dominic Cooney ac7e06fcab Deleting tasks is reflected immediately in history. 2026-05-04 11:33:05 -07:00
Dominic Cooney 5eee05aafe fix: S6-24 tool input preservation, S6-6 path mismatch, restore streaming state push
- S6-24: Preserve tool input from content_start for use at content_end
  (MessageTranslatorState stores streamingToolInput/streamingToolName)
- S6-6: Replace readUiMessages (legacy path) with getSavedClineMessages
  (uses HostProvider.globalStorageFsPath, matching saveClineMessages)
- Restore postStateToWebview() in handleSessionEvent (needed for streaming)
- Fix TS2352 cast in message-translator.ts (as unknown as Record)
- Add 4 new tests for tool input preservation through streaming lifecycle
- S6-26: Research SDK pending prompts/tool approval/ask_question system
- S6-27: Create focused task for history messages still not rendering
- Update PROBLEMS.md priority section
2026-05-04 11:33:05 -07:00
Dominic Cooney 9d371572cd fix: display inference messages in webview (SDK migration)
The partial message handler in ExtensionStateContext only updated
existing messages by matching timestamps — it never appended new ones.
In the classic extension, messages were first added via state updates,
then updated in-place by partial messages. In the SDK migration,
messages arrive via the partial message stream first, so they need
to be appended when no existing message matches.

Also adds debounced ClineMessage persistence in SdkController so
task history can load messages via readUiMessages().
2026-05-04 11:33:05 -07:00
Dominic Cooney 3de6eeff6b docs: update PROBLEMS.md with verification results and new issues
- S6-5: Regressed — inference works but view doesn't switch to chat
- S6-6: Failed verification, merged S6-15 into it
- S6-8: Marked as verified fixed (brown logo)
- S6-19: New — history deletion dialog confirms but doesn't delete
- S6-20: New — MCP tools panel is empty
- Added Priority & Next Steps section recommending S6-5 as top priority
2026-05-04 11:33:05 -07:00
Dominic Cooney 008a66889f fix: build system prompt for SDK sessions to enable inference
The SDK's DefaultSessionManager passes the systemPrompt from
CoreSessionConfig directly to the Agent — there is no fallback for
empty system prompts. Both the CLI and SDK's VSCode extension call
buildClineSystemPrompt() before passing config to the session manager.

Our buildSessionConfig() was setting systemPrompt: '' (empty string),
which caused the gateway to return empty responses with 0 tokens.

Changes:
- cline-session-factory.ts: Import buildWorkspaceMetadata from
  @clinebot/core and buildClineSystemPrompt from @clinebot/shared.
  Build the full Cline system prompt in buildSessionConfig() using
  workspace metadata, IDE name, mode, provider, and platform.
  Falls back to a minimal prompt on error.
- vscode-session-host.ts: Add logging around send() for debugging
  inference issues (input/output tokens, response text).
- SdkController.ts: Add logging for session events and agent turn
  completion to aid debugging.

Verified: Agent responds correctly with 2776 input tokens (system
prompt) and generates proper output ('Hello, world! 👋').
2026-05-04 11:33:05 -07:00
Dominic Cooney 992feabd21 document new problems: S6-15 through S6-18
- S6-15: History items not clickable (blocker)
- S6-16: Sending message completes immediately with no output (blocker)
- S6-17: Cancel button enabled after task completes (minor)
- S6-18: Missing API key shows error instead of login prompt (blocker)
2026-05-04 11:33:05 -07:00
Dominic Cooney ccb5095877 simplify oauth: read credentials from providers.json directly
- Rewrite restoreRefreshTokenAndRetrieveAuthInfo() to read from providers.json
  instead of injecting through StateManager secrets
- Add fetchUserInfoFromApi() to get user profile from Cline API at startup
- Delete VscodeOAuthTokenManager (~180 lines) - SDK default handles persistence
- Simplify resolveApiKey() for cline provider to read from providers.json
- Cache getProviderSettings() as singleton to avoid re-reading file
- Net reduction of 94 lines
2026-05-04 11:33:04 -07:00
Dominic Cooney 257f1824b6 Add codebase search guidance for avoiding build output
Documents which directories contain minified/generated code that
produces noisy search results (out/, dist/, src/generated/, etc.),
how to skip them with search_files and grep, and how to search
minified files when necessary (grep -oP, source maps).
2026-05-04 11:33:04 -07:00
Dominic Cooney 6932ba829b sdk: wire VscodeSessionHost into SdkController for end-to-end inference
Replace ClineCore.create() with VscodeSessionHost.create() which
constructs DefaultSessionManager directly with VSCode-specific options:

1. VscodeSessionHost (new file):
   - Implements SessionManager interface, wrapping DefaultSessionManager
   - Injects source: 'vscode' on start() for telemetry tagging
   - Writes empty MCP settings to prevent SDK's default MCP loading

2. VscodeOAuthTokenManager:
   - Reads Cline OAuth tokens from secrets.json (cline:clineAccountId)
   - Refreshes tokens via SDK's refreshClineToken()
   - Returns null on re-auth failure instead of throwing
     OAuthReauthRequiredError (prevents 'Run clite auth' error)
   - Delegates non-cline OAuth (openai-codex, oca) to SDK's
     ProviderSettingsManager

3. VscodeRuntimeBuilder (from previous commit, now wired in):
   - Bridges classic McpHub to SDK tool system
   - Supports all MCP transports (stdio, SSE, streamableHttp)

4. SdkController changes:
   - initTask() uses VscodeSessionHost.create({ mcpHub })
   - ActiveSession.core -> ActiveSession.sessionManager
   - Removed createClineCore() and MCP filtering from session factory

All 99 SDK unit tests pass. No new TypeScript errors.
2026-05-04 11:33:04 -07:00
Dominic Cooney b378b6e4a4 sdk: fix webview state, chat history, and add VscodeRuntimeBuilder
Four fixes for making inference work in the VSCode UI:

1. Webview state connection (S6-13): Wire WebviewGrpcBridge to the
   controller's getStateToPostToWebview() so state updates include
   messages, currentTaskItem, and task history. Added setGetStateFn()
   method to the bridge and called it from SdkController constructor.

2. Chat history loading (S6-6): Fix history lookup in showTaskWithId()
   and reinitExistingTaskFromId() to check StateManager's taskHistory
   first (where updateTaskHistory writes), then fall back to the legacy
   file reader. Also clear active session before viewing history.

3. Message translation (S6-12): Add sdkToolToClineSayTool() mapping
   function that converts SDK tool names (read_files, editor, etc.) to
   classic ClineSayTool format that ChatRow.tsx expects. Add usage event
   handling for api_req_started messages. Update tests to match actual
   output format.

4. VscodeRuntimeBuilder (S6-14): New custom RuntimeBuilder that bridges
   the classic McpHub to the SDK's tool system. Delegates builtin tools
   to DefaultRuntimeBuilder but replaces MCP tools with ones loaded from
   the classic McpHub, supporting all transport types (stdio, SSE,
   streamableHttp). Not yet wired into session creation — needs
   VscodeSessionHost wrapper (see S6-9).

All 99 SDK unit tests pass.
2026-05-04 11:33:04 -07:00
Dominic Cooney f4e8f89ca5 feat(sdk): fix inference pipeline — credential resolution, non-blocking start, MCP filtering
- Fix credential resolution: replace broken ProviderSettingsManager and
  buildApiHandlerSettings() paths with resolveApiKey()/resolveModelId()
  that read directly from StateManager.getApiConfiguration() (includes
  secrets). Handles all 30+ providers including cline OAuth token
  extraction (idToken from cline:clineAccountId JSON, workos: prefix).

- Fix non-blocking session start: initTask() now calls
  core.start({interactive:true}) WITHOUT a prompt (returns immediately),
  then fire-and-forgets core.send() for inference. Events stream
  in real-time via subscribe(). Same pattern for askResponse().

- Add MCP settings filtering: SDK's StdioMcpClient only supports stdio
  transport. ensureFilteredMcpSettings() writes a filtered copy of
  cline_mcp_settings.json (stdio-only) and sets CLINE_MCP_SETTINGS_PATH
  env var. Future: replace with custom RuntimeBuilder that provides a
  clientFactory delegating to classic McpHub for streamableHttp support.

- Fix debug harness gRPC message format for web.post_message.

- Update PROBLEMS.md: S6-5 and S6-11 marked 🟢 Verified Fixed.

Verified: Debug harness sends newTask → session starts, agent runs,
events stream to webview (iteration_start, usage, iteration_end, done).
2026-05-04 11:33:04 -07:00
Dominic Cooney a67ccc59d4 Add Claude analysis of the branch. 2026-05-04 11:33:04 -07:00
Dominic Cooney aefa0b3f7a Fix: empty clineEnv in updateSettings flips to local environment
Protobuf defaults empty strings to ''. The check 'request.clineEnv !== undefined'
was true for empty strings, causing ClineEnv.setEnvironment('') which defaults
to 'production' but also triggers accountLogoutClicked(). This caused the user
to be logged out and the environment to appear to flip when changing models.

Fix: Also check 'request.clineEnv !== ""' before processing.
2026-05-04 11:33:04 -07:00
Dominic Cooney ecddcef2a5 Fix critical bugs: history item saving, VSCode API exposure, logging
1. initTask() now saves history item to StateManager immediately
   after session starts. Without this, currentTaskItem was undefined
   in getStateToPostToWebview(), so the webview never switched to
   chat view after sending a message.

2. updateTaskHistory() and deleteTaskFromState() are now real
   implementations using StateManager instead of stubs.

3. Exposed window.__clineVsCodeApi in webview for debug harness
   access (platform.config.ts).

4. Added detailed logging to initTask() for debugging:
   - Session config (provider, model, apiKey presence)
   - ClineCore creation
   - Session start
   - Error details

These fixes address the user-reported issues:
- Send button clears input but doesn't switch to chat view
- Chat history appears empty
2026-05-04 11:33:04 -07:00
Dominic Cooney 001399b9a4 Update README with Step 7 & 8 progress 2026-05-04 11:33:03 -07:00
Dominic Cooney bf461f19da Step 8: Settings & Features — TaskProxy compatibility + togglePlanActMode
TaskProxy improvements for settings handler compatibility:
- api property is now settable (updateSettings replaces it on model switch)
- terminalManager returns a proper stub that safely no-ops
  (setDefaultTerminalProfile, setShellIntegrationTimeout, etc.)
- Added TaskProxyTerminalManager interface

SdkController improvements:
- togglePlanActMode() now properly implemented:
  - Saves mode to StateManager
  - Cancels active task if switching modes mid-task
  - Posts state update to webview
- toggleActModeForYoloMode() switches to act mode

These changes ensure the updateSettings gRPC handler works
without modification — it calls controller.task.api and
controller.task.terminalManager which are now properly stubbed.
2026-05-04 11:33:03 -07:00
Dominic Cooney 3f893b1040 Step 7: Wire classic McpHub into SdkController
Following the 'Thunk, Don't Replace' principle, wire the classic
McpHub into SdkController instead of building a custom SDK MCP
manager. The classic McpHub already supports all three transports
(stdio, SSE, streamableHTTP), file watching, and all gRPC handlers.

Changes:
- SdkController.mcpHub: type changed from 'any' to 'McpHub'
- Constructor initializes McpHub with same args as classic Controller
- Existing gRPC handlers (subscribeToMcpServers, restartMcpServer,
  deleteMcpServer, toggleMcpServer, etc.) work without modification
- SDK's InMemoryMcpManager will replace it in Step 10 (Cleanup)

Also includes:
- Fix S6-5: buildSessionConfig() falls back to classic StateManager
  when SDK ProviderSettingsManager has no provider configured
- Fix S6-6: showTaskWithId() loads messages from disk via
  readUiMessages() and adds them to TaskProxy's messageStateHandler
- Updated PROBLEMS.md and README.md with Step 7 progress
2026-05-04 11:33:03 -07:00
Dominic Cooney 88ae634fa1 Fix S6-5 & S6-6: provider config fallback + history message loading
S6-5: buildSessionConfig() now falls back to classic StateManager
- Try SDK ProviderSettingsManager first (providers.json)
- If no provider/apiKey found, fall back to StateManager.buildApiHandlerSettings()
- This correctly resolves provider/model/apiKey for the current mode (plan/act)
- Critical because providers.json may not exist yet for existing users

S6-6: showTaskWithId() now loads messages from disk
- Call readUiMessages(taskId) to load ui_messages.json
- Add messages to TaskProxy's messageStateHandler via addMessages()
- This populates the message state so getStateToPostToWebview() returns them

Also: remove unused Settings import from cline-session-factory.ts,
add StateManager import for the fallback path.
2026-05-04 11:33:03 -07:00
Dominic Cooney a04e204c1b Step 6: Auth & Account Flows — SDK-backed auth and account services
- Add src/sdk/auth-service.ts: SDK-backed AuthService replacing classic AuthService
  - loginClineOAuth(), loginOcaOAuth(), loginOpenAICodex() via SDK functions
  - Token persistence to secrets.json with workos: prefix
  - Cross-window auth sync via secrets change listener
  - Streaming subscriptions with immediate initial state push
- Add src/sdk/account-service.ts: SDK-backed ClineAccountService
  - Authenticated API requests using SDK-backed AuthService
  - Credit fetching, org switching, payment history
- Wire gRPC handlers to SDK-backed auth:
  - accountLoginClicked, accountLogoutClicked, subscribeToAuthStatusUpdate
  - openAiCodexSignIn, openAiCodexSignOut
- Update SdkController to initialize auth/account services
- Update extension.ts secrets listener to use new auth-service
- 20 unit tests in auth-service.test.ts
- Update sdk-migration/README.md and PROBLEMS.md for Step 6

Status: Implementation complete, awaiting E2E verification
Blockers: S6-5 (inference not starting), S6-6 (history items not loading)
2026-05-04 11:33:03 -07:00
Dominic Cooney 86aa1a68ef Step 5: gRPC thunking layer — TaskProxy + WebviewGrpcBridge
- src/sdk/task-proxy.ts: TaskProxy provides classic Task-compatible
  interface that delegates to SDK session methods. MessageStateHandler
  extends EventEmitter for CLI compatibility (on/off pattern).
  TaskProxyState mirrors classic TaskState subset.

- src/sdk/webview-grpc-bridge.ts: Bridges SDK session events to
  webview gRPC streams. Translates ClineMessages to proto format
  and pushes through sendPartialMessageEvent/sendStateUpdate.

- src/sdk/SdkController.ts: Wired TaskProxy + WebviewGrpcBridge
  into session lifecycle. Events flow: SDK → message translator →
  gRPC bridge → webview streams. Reuses getStateToPostToWebview()
  for state building.

- No 'as' casts in production code — type narrowing used instead.
  Stubs throw errors instead of returning undefined as unknown as T.

- 114 unit tests pass across 6 test files.
- 0 new TypeScript errors (3 pre-existing in unrelated files).
- Extension loads, sidebar renders, newTask routes correctly.
- initTask fails at runtime because ClineCore.create() needs SDK
  config (Step 6+).
2026-05-04 11:33:03 -07:00
Dominic Cooney a14b4c9088 Step 4: Session lifecycle — SDK adapter layer
Implement session lifecycle for the SDK migration:

- src/sdk/cline-session-factory.ts: Build CoreSessionConfig from
  legacy state via ProviderSettingsManager, create ClineCore instances,
  build StartSessionInput/resume inputs, HistoryItem CRUD helpers

- src/sdk/message-translator.ts: Translate all SDK CoreSessionEvent
  types to ClineMessage[] for webview consumption. Handles chunk,
  agent_event (content_start/update/end, done, error, notice, usage),
  ended, hook, status events. Streaming state tracking for partial
  message dedup.

- src/sdk/SdkController.ts: Session lifecycle methods (initTask,
  askResponse, cancelTask, clearTask, showTaskWithId,
  reinitExistingTaskFromId), SDK event subscription/translation
  pipeline, session event listener system.

- Tests: 91 unit tests across 4 files (27 message-translator,
  37 legacy-state-reader, 15 cline-session-factory, 12
  provider-migration). TypeScript compiles with 0 errors.

- Updated sdk-migration/README.md: Step 4 marked completed,
  improved debug harness overlay dismiss instructions.

- Updated sdk-migration/PROBLEMS.md: Step 4 verified, 3 minor
  known issues documented (S4-1, S4-2, S4-3).
2026-05-04 11:33:02 -07:00
Dominic Cooney 79a51cc5f0 Step 3: Provider Migration — SDK-backed credential migration
Implements src/sdk/provider-migration.ts with:
- migrateProviders() using SDK's ProviderSettingsManager auto-migration
- getProviderSettingsManager() for accessing provider settings
- Supports all 30+ providers (Anthropic, OpenAI, OpenRouter, Bedrock, Ollama, Cline, etc.)
- Never overwrites existing entries (idempotent)
- Tags migrated entries with tokenSource: 'migration'
- 12 unit tests passing, 0 TypeScript errors
2026-05-04 11:33:02 -07:00
Dominic Cooney 7a1be66de8 Step 2: Legacy State Reader — read all on-disk state from SDK adapter layer
Implements src/sdk/legacy-state-reader.ts with:
- readGlobalState/readGlobalStateKey for globalState.json
- readSecrets/readSecretKey for secrets.json
- readTaskHistory for state/taskHistory.json
- readApiConversationHistory, readUiMessages, readContextHistory, readTaskMetadata for per-task data
- readMcpSettings for settings/cline_mcp_settings.json
- listTaskIds for task directory listing
- readAllLegacyState composite reader
- All reads are non-throwing (missing/corrupt files return typed defaults)
- 37 unit tests passing, 0 TypeScript errors
2026-05-04 11:33:02 -07:00
Dominic Cooney ce2dcfb402 Step 1: Foundation & Cutover - SDK adapter layer
- Add @clinebot/core, @clinebot/llms, @clinebot/shared, @clinebot/agents
  as dependencies via file: protocol (linked to ../sdk-wip)
- Create src/sdk/ directory with SdkController stub and barrel export
- Replace src/core/controller/index.ts with re-export from SDK adapter
  (classic Controller accessible via origin/main)
- Extract getStateToPostToWebview() to standalone function for reuse
- Add vitest.config.sdk.ts for SDK adapter tests
- Fix implicit any types in handler modules
- Extension compiles and builds successfully (tsc + esbuild pass)
- Single entry point: no CLINE_SDK flag, SDK adapter is the only codepath

Replaces classic src/core/controller/index.ts (see origin/main)
2026-05-04 11:33:02 -07:00
Dominic Cooney e291f4067a sdk-migration-v3: single entry point, delete-and-document principle
Key changes from feedback:
- Replace 'don't delete what you haven't replaced' with 'delete and document'
  - Delete classic code immediately when replaced by SDK equivalent
  - Add 'Replaces classic src/core/... (see origin/main)' comments
  - Use kb_search/git to reference origin/main for classic implementation
- Single entry point: no CLINE_SDK env variable, no dual codepaths
  - Step 1 now modifies src/extension.ts directly
  - Rationale: dual entry points caused constant confusion in attempt 2
- Updated ARCHITECTURE.md with key architectural decisions
- Updated .clinerules/sdk-migration.md with new rules
2026-05-04 11:33:02 -07:00
Dominic Cooney 99126ce210 sdk-migration-v3: seed the third migration attempt
- Port forward debug harness (server.ts, README.md, .clinerules)
- Port forward ws.d.ts type declaration
- Create sdk-migration/ doc structure:
  - README.md: entry point, 10-step plan, operational procedure
  - ARCHITECTURE.md: features, design decisions, SDK capabilities
  - SDK-REFERENCE/OAUTH.md: SDK OAuth reference with pitfalls
  - SDK-REFERENCE/MCP.md: SDK MCP reference with gap analysis
  - PROBLEMS.md: issue tracker with verification requirements
- Add .clinerules/sdk-migration.md for agent guidance

Key changes from attempt 2:
- gRPC thunking instead of typed message replacement
- Verification gates before each step
- Never delete what you haven't replaced
- Structured problem tracking with evidence requirements
- Concise, purpose-specific docs with fan-out structure
2026-05-04 11:33:02 -07:00
Mikołaj Kondratek 90c8112257 At-mention picker: show "Searching..." instead of misleading "No results found" (#10478)
* At-mention picker: show "Searching..." instead of misleading "No results found"

When the @-mention picker fires its initial empty-query searchFiles, slow
workspaces (e.g. network mounts) leave the call in flight for several
seconds. Three small UX bugs combined to make this look broken:

1. The 500ms delayed-loading effect was gated on `searchQuery` being
   non-empty, so the spinner never appeared during the initial open —
   the user just saw "No results found" forever.
2. While loading, the spinner row stacked above the "No results found"
   row, claiming both states at once.
3. The spinner also stacked above the static root-menu items
   ("Paste URL", "Problems", "Git Commits", "Add File", "Add Folder")
   when the picker first opens with empty input, even though those
   items are already actionable.

Fixes:
- Drop the `&& searchQuery` guard so the loading effect arms on empty
  queries too.
- In `filteredOptions`, strip the lone `NoResults` entry while
  `showDelayedLoading` is true — searching is not the same as nothing
  matched.
- Render the spinner only when `filteredOptions.length === 0`, so it
  never stacks above existing options.

The 500ms delay before the spinner appears is preserved, so fast
searches stay visually quiet.

* fixes

* Drop stale @-mention searchFiles responses to fix "No results" flash

* Track in-flight searches with a monotonic latestSearchTokenRef in
  ChatTextArea; resolve/error handlers bail when their captured token
  is no longer the latest.
* Send the token as mentionsRequestId; proto already supports it.
* Drop the never-read currentSearchQueryRef scaffold.
* Fixes the cancel-then-re-pick race (Add File → cancel → Add Folder)
  reported in CLINE-1814.
2026-05-04 15:46:28 +02:00
Max 86f463496c bump versions and changelog (#10503)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-05-01 09:25:09 -07:00
Saoud Rizwan 72562ea74e feat: add beta version checkbox to bug report issue template (#10490)
Add a checkbox for users to indicate they're on a beta version, and
auto-apply the 'beta' label via the existing auto-label workflow when
the checkbox is checked.
2026-04-30 17:35:32 -07:00
Max 544e3aa240 publish sdk migration branch to nightly main channel (#10485)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-04-30 12:41:43 -07:00
Dominic Cooney beb3ad78dc Revert "Remove foreground terminal from Cline VSCode extension (#10196)" (#10477)
This reverts commit 1862f15955.
2026-04-30 09:41:52 -07:00
Mikołaj Kondratek ee1d4b4dcf CLINE-1814 typed RipgrepSpawnError + error_reason proto (#10443)
* CLINE-1814 typed RipgrepSpawnError + error_reason proto

* file-search.ts: define RipgrepSpawnError carrying stderr+exitCode; reject
  on non-zero ripgrep exit (with empty results) instead of resolving to []
* file-search.ts: re-throw from searchWorkspaceFiles and
  searchWorkspaceFilesMultiroot so the controller sees the error and can
  attach a structured error_reason to the proto response
* file-search.test.ts: assert spawn-time and exit-time errors both surface
  as RipgrepSpawnError
* file.proto/FileSearchResults: add optional error_reason and error_message
  fields with the closed enumeration of values documented inline

Phase 1 of the visibility patch. No behaviour change for healthy installs;
broken installs now surface a real error instead of an empty list.

Refs: CLINE-1814

* CLINE-1814 surface error_reason in picker UI

Controller (searchFiles.ts):
* classify thrown errors into a closed enumeration of error_reason values
  (workspace_unavailable, ripgrep_spawn_failed, unknown). RipgrepSpawnError
  unwraps the first line of stderr into error_message so the picker can
  surface ENOENT / EACCES / 'Operation not permitted' verbatim.
* the no-workspace-path branch now returns workspace_unavailable instead
  of an empty result list.
* keep using telemetry.captureMentionFailed for aggregate signal but map
  ripgrep_spawn_failed -> 'unknown' to stay within the existing closed enum.

Webview (ChatTextArea + ContextMenu):
* ChatTextArea threads errorReason / errorMessage from each searchFiles
  RPC response (and from RPC-level rejections) into ContextMenu.
* ContextMenu renders a grey, italic, smaller subtitle beneath the
  'No results found' row when an error_reason is present.
* renderErrorSubtitle() carries a short doc-comment for each value so
  reviewers can see at a glance how each error_reason maps to UI copy.

Refs: CLINE-1814

* CLINE-1814 trim verbose CLINE-1814 ticket-reference comments

Pure code-quality pass over the Phase 1 changes: condense the long
narrative comments that referenced the ticket into terser explanatory
comments where they still add value, and remove ones that just
repeated what the (now-stable) code already says. No behaviour change.

* CLINE-1814 fix RipgrepSpawnError override of Error.cause

TS error 'This member must have an override modifier because it
overrides a member in the base class Error' - Error gained an optional
'cause' field in ES2022. Drop the explicit field declaration and pass
the cause via the standard ES2022 ErrorOptions in super(). Behaviour
unchanged: instances still expose .cause via the base-class field.

* CLINE-1814 fix Windows race in executeRipgrepForFiles finalisation

CI hit this on Windows:

  AssertionError: expected [Promise] to be rejected with a message
  matching /ripgrep exited with code 2/, but got 'ripgrep exited with
  code null: rg: /bogus: No such file or directory (os error 2)'

The readline 'close' event and the child-process 'exit' event fire in
non-deterministic order on Windows. The previous code keyed off 'close'
alone, which meant the rejection branch could run with exitCode still
null even when the process eventually exited with a real code.

Fix: gate finalisation on both events with a small barrier (rlClosed +
processExited + finalised flags). Idempotent and safe under any
ordering. Test passes on darwin (where the ordering used to be benign)
and the same path now produces the expected exitCode=2 message on
Windows.

No production-behaviour change on the happy path: results still resolve
exactly when the readline finishes parsing stdout.

* CLINE-1814 address Phase 1 code-review feedback

Three small follow-ups from review on the Phase 1 PR:

1) ContextMenu.tsx: drop a stray trailing semicolon on the
   selectedType prop declaration so the interface style stays
   consistent with the rest of the file (no semicolons on field
   declarations). Cosmetic only, no behaviour change.

2) file-search.ts: in executeRipgrepForFiles' rgProcess.on('error')
   handler, set finalised = true before reject() so that a subsequent
   ('close', 'exit') pair can't pass the finalise() guards. The
   double-reject was already a no-op (Promises swallow further
   reject() calls once settled), but unconditionally maintaining the
   barrier invariant makes the lifecycle of this Promise much easier
   to reason about and matches the symmetry of the other two
   finalise() callers.

3) file-search.test.ts: collapse the awaited-twice rejected-promise
   pattern in 'should reject with RipgrepSpawnError when ripgrep
   exits non-zero with no results'. The previous form
   (await should(p).be.rejectedWith(...); await p.catch(...)) worked
   because settled promises replay their value, but it's subtly
   misleading. The new form awaits once via .catch() and asserts on
   the resulting error directly.

All six File Search unit tests continue to pass.

* CLINE-1814 revert picker error subtitle (UI for impl-detail leak)

Per code-review feedback: surfacing structured error_reason / error_message
from FileSearchResults as a grey-italic subtitle on the 'No results found'
row exposes implementation detail to end-users. The user can't act on
'(ripgrep failed: rg: ENOENT)' or '(internal error: spawn EACCES)' — those
are diagnostic data that belong in logs and aggregate telemetry.

Reverted in this commit:
  - ContextMenu.tsx: errorReason / errorMessage props removed,
    renderErrorSubtitle helper deleted, NoResults row reverts to a plain
    <span>No results found</span>.
  - ChatTextArea.tsx: searchErrorReason / searchErrorMessage state and
    all setSearchErrorReason / setSearchErrorMessage call-sites removed;
    ContextMenu invocation no longer passes the two props.

Kept on purpose:
  - The proto field FileSearchResults.error_reason — it's harmless on
    the wire and the next commit wires it up to telemetry + structured
    logging, which is where this signal actually belongs.
  - The classifyError helper and ERROR_REASON_* constants in the
    searchFiles controller — same reason, they feed telemetry next.

Six file-search unit tests still pass.

* CLINE-1814 telemetry: surface ripgrep_spawn_failed / workspace_unavailable

Until now the searchFiles controller's catch block collapsed every classified
error_reason — workspace_unavailable, ripgrep_spawn_failed, unknown — onto a
two-value telemetry enum (permission_denied | unknown), throwing away the
diagnostic signal we'd worked hard to extract. The Linear ticket explicitly
asks for the structured signal to feed telemetry; this commit delivers on that.

Changes:
  - TelemetryService.captureMentionFailed: extend the errorType enum with
    two new categorical values, ripgrep_spawn_failed and workspace_unavailable.
    Doc-comment updated to call out that those two are picker-search failures
    (vs the existing values which are mention-content retrieval failures).
  - searchFiles.ts:
      * empty-workspace branch now emits errorType=workspace_unavailable
        (previously: not_found).
      * catch-block computes errorType from the classified errorReason —
        RipgrepSpawnError -> ripgrep_spawn_failed, EACCES -> permission_denied,
        otherwise unknown — instead of always permission_denied | unknown.

Net result: ops can now distinguish 'ripgrep is broken on this user's
machine' from 'user is in a one-window-no-folder IntelliJ session' from
genuine code bugs in the search pipeline.

* CLINE-1814 log: include classified errorReason on searchFiles error line

Trivial follow-up to the previous commit. Triagers grepping
~/.cline/cline-core-service.log for searchFiles failures got the raw error
object dumped, but no hint as to which of the structured error_reason
buckets the failure falls into. Now the log line is

    [ERROR] Error in searchFiles (errorReason=ripgrep_spawn_failed): <Error...>

so a single grep for 'errorReason=ripgrep_spawn_failed' surfaces every
ripgrep-side failure across the user's session without having to read the
stack trace. Same field value as the gRPC response and the telemetry event,
so the three sources can be cross-referenced in incident triage.

Also moved the classifyError() call above the Logger.error() call (was
below) so the reason is computed once, used twice.

* CLINE-1814 address Phase 1 review feedback (rename, simplify, trim comments)

Five review comments rolled into one commit:

file-search.ts
  * Rename RipgrepSpawnError -> RipgrepError. The class covers spawn failures
    AND non-zero-exit / stderr-on-empty-stdout paths; the old name only
    described half its job.
  * Drop the unread 'cause' constructor option. We were never reading
    err.cause anywhere downstream, and the only producer was the
    spawn-error path which already encodes the underlying message in the
    string.
  * Replace the rlClosed/processExited/finalised state machine with two
    Promise resolvers awaited via Promise.all. Same ordering guarantees on
    Windows (both 'close' and 'exit' must fire before we settle), zero
    mutable bookkeeping, and the spawn-error path no longer needs to
    pre-flip a 'finalised' flag to be safe against a late close+exit pair.
    Used new Promise() rather than Promise.withResolvers() because TS lib
    is es2022 and withResolvers is es2024; behavior is identical.

searchFiles.ts
  * .trim() the stderr before split() so a leading newline doesn't yield
    an empty first line on the telemetry / log path.
  * Drop the 'this commit' comment (ephemera once 'this commit' is no
    longer the most recent one) and the 'Determine mention type based on
    the search request' comment, both of which restated the obvious code.

file-search.test.ts
  * Update test name and assertion to match the renamed class.

All 6 file-search unit tests pass.

* CLINE-1814 drop unread RipgrepError.exitCode field

Follow-up to the previous review-feedback commit. The 'we're not reading
this anywhere' note was about exitCode, not cause - my mistake. The exit
code is already encoded into the error message string ('ripgrep exited
with code N: <stderr>'), so the dedicated field was carrying no
additional information for any consumer.

Dropped:
  * RipgrepError.exitCode field and constructor option
  * 'exitCode' from the two new RipgrepError(...) call sites
  * 'should(err).have.property(exitCode, 2)' from the unit test

The internal exitCode local in executeRipgrepForFiles stays - it gates
the reject vs resolve decision after both 'close' and 'exit' have fired.
It's just no longer plumbed onto the error.

All 6 file-search unit tests still pass.
2026-04-29 10:42:48 -07:00
Renee Huang f2eda58c70 rm hardcoded model lists (#10436)
* rm hardcoded model lists

* Apply suggestion from @greptile-apps[bot]

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-04-27 17:26:30 -07:00
Ara 4afc973f7d feat(openai): add latest native models (#10435) 2026-04-27 16:16:19 -07:00
Ara 5fe6c9a8ce Add Z AI GLM-5.1 model (#10409) 2026-04-25 11:11:32 -07:00
Dominic Cooney 901d1b5c97 fix(hooks): Use shell escapes on JSON literals in hooks templates (#10382)
* Fix quote escaping in hooks templates.

* Bump timeouts.

* Escaping for CONTEXT_MOD.

* Fix documentation and existing templates to use correct input property names.
2026-04-24 11:11:44 -07:00
tjandy98 c139f7a4d5 Add GPT-5.4 and GPT-5.4-nano (#10394)
Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>
2026-04-24 18:02:47 +02:00
Max 07593bb42a version bump and changelog (#10395)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-04-24 08:16:35 -07:00
Ara fd21c314c1 Adding Gpt-5.5 to OpenAI codex subscription provider (#10390) 2026-04-24 07:48:47 -07:00
Tomás Barreiro 32ca1cad9a Use env for github inputs (#10383) 2026-04-24 02:12:27 -07:00
Tomás Barreiro 852c65b70c Do not show hardcoded new items (#10374)
* Do not show hardcoded new items

* fix use effect dependencies

* fix tests
2026-04-23 11:52:03 -07:00
Mikołaj Kondratek 70f0e8d548 feat(memory-observability): add periodic memory logging to cline-core (#10343)
* feat(memory-observability): add periodic memory logging to cline-core

Introduces a lightweight memory monitor that logs process.memoryUsage()
snapshots to the existing cline-core log every 5 minutes, plus an
immediate baseline at startup and a final snapshot at graceful shutdown.

Each entry is written as a single `[MEMORY] key=valueMB ...` line so it
is trivially greppable and parseable:

    grep '\[MEMORY\]' ~/.cline/cline-core-service.log

The timer is unref()'d so it does not keep the event loop alive on its
own, ensuring the Node process can still exit cleanly.

Also adds an informational log line after process.chdir(__dirname) that
records where V8 will write heap snapshots if --heapsnapshot-near-heap-limit
triggers them, and a best-effort process.on("exit") handler that scans
cwd for .heapsnapshot files on abnormal exit and logs their paths/sizes
so post-mortem investigation starts with the diagnostic data in hand.

This is Part 1 (periodic memory logging) and the Node-side portions of
Part 2 (snapshot directory + exit handler) of the memory observability
implementation plan. The V8 flag itself and the
~/.cline/heapsnapshots/ move-and-cap cleanup live in the Kotlin
CoreProcessManager and are applied separately in the plugin repo.

No business-logic changes; purely additive diagnostics.

* chore(memory-observability): enable --heapsnapshot-near-heap-limit=3 in runclinecore.sh

When cline-core approaches the V8 heap ceiling, V8 will now write up to
3 .heapsnapshot files to the current working directory before giving up
and crashing. These snapshots can be loaded into Chrome DevTools → Memory
tab to identify the objects retaining the most memory.

N=3 is chosen because the last snapshot (written just before the fatal
OOM) shows only live, truly-unreclaimable objects — the earlier ones still
contain garbage the GC hadn't collected yet. Having all three lets us
compare.

This flag is a V8 runtime flag and must be passed on the node command
line; it cannot be enabled from JavaScript at runtime.

Matches the equivalent change on the cline-core launcher in the IntelliJ
plugin repo (CoreProcessManager.kt).

* chore(memory-observability): reduce --heapsnapshot-near-heap-limit from 3 to 1

Reviewer concern: with --max-old-space-size=8192, each heap snapshot
serializes at roughly 4-5x heapUsed on disk, so three snapshots can
burst 24-40 GB to disk in the seconds before an OOM crash — right
when the system is already under memory/CPU pressure. On a laptop
with <40 GB free this can leave partial/corrupted snapshots or
trigger OS pressure on unrelated processes.

The plan doc originally argued 'snapshot 3 of 3 is most valuable
because it contains only live objects'. In practice, by the time V8
triggers the flag it has already run aggressive mark-compact cycles,
so snapshot 1 is nearly-all-live too. Our own Scenario B verification
run confirmed that even the first snapshot contained the retainer
chain — snapshots 2 and 3 added no diagnostic signal.

Trade-off:
  - per-OOM disk burst:      24-40 GB  ->  8-14 GB  (3x reduction)
  - time-to-crash (frozen):  30-60 s   ->  10-20 s  (3x reduction)
  - diagnostic signal:       essentially unchanged

The persistent-directory cap in CoreProcessManager.kt stays at 3, so
we still retain snapshots from the 3 most recent OOM events for
cross-event comparison.

* chore(memory-observability): shorten runclinecore.sh flag comments

The one-line pointer to CoreProcessManager.kt was more noise than
signal given the flags are visible on the same line as the command.
Rationale for the --heapsnapshot-near-heap-limit value lives in the
Kotlin constant's KDoc and in the commit log.
2026-04-23 13:34:51 +09:00
TheRealSpencer 5accd88d73 fix: pin protobufjs to 7.5.5 to address CVE-2026-41242 (#10365) 2026-04-22 20:22:32 -07:00
Saoud Rizwan 5b29be63b8 docs: remove demo video from README (#10363) 2026-04-22 18:46:17 -07:00
Max 9dea336ced update changelog (#10356)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-04-22 11:03:35 -07:00
Tomás Barreiro 697f801937 Use the recommended model list for the onboarding flow (#10355)
* Use the recommended model list for the onboarding flow

* show the welcome view if we fail to load models
2026-04-22 19:32:45 +02:00
Tomás Barreiro 6be35bfcea Remove old hardcoded banners (#10354)
* Remove old hardcoded banners

* Remove tests
2026-04-22 10:21:01 -07:00
Ara 5a91800b6c Add SDK nightly publish workflow (#10344)
* chore: add sdk nightly publish workflow

* chore: publish sdk nightly to prerelease channel
2026-04-22 10:13:56 -07:00
Jose Castelli dacadbaae0 use details instead of catched error message (#10353)
use details instead of caught error message
2026-04-22 16:50:45 +02:00
Dominic Cooney 8d020e89e6 chore: Publish regular nightlies to Cline (Nightly) *release* channel (#10338)
* Make the nightly publishing script use the stable channel of cline-nightly.

* Address PR review feedback from Greptile and Copilot

- Reject unknown CLI flags with an error message, preventing typos like
  --prerelease from silently publishing to the wrong channel (Greptile)
- Rename 'stable' to 'release' throughout docs, help text, and log
  messages to match VS Code Marketplace terminology (Copilot)
- Rename workflow step from 'Publish Extension as Pre-release' to
  'Publish Nightly Extension' since it now publishes to the release
  channel by default (Greptile)
2026-04-21 21:26:57 -07:00
Jose Castelli c6dbc8bcb0 Adding cline quota exceeded cap error message (#10323)
Adding cline quota exceeded cap error message
2026-04-21 11:51:40 +02:00
CandiedUniverse 1862f15955 Remove foreground terminal from Cline VSCode extension (#10196)
* Create implementation plan doc

* Remove foreground terminal UI and default task execution to background mode

* Remove terminal mode UI service endpoint

* Remove foreground terminal mode state and RPC surface

* Add terminal settings UI regression test

* Guard removed foreground terminal state keys

* Test simplified terminal command routing

* Remove dead terminal profile plumbing

* Remove stale terminal mode references

* Add terminal settings verification story

* Remove implementation plan doc once implemented

* fix e2e launch under electron-run-as-node

* address greptile terminal follow-ups

* address greptile proto and vscode terminal notes

* address greptile test follow-ups

* remove dead acp terminal stubs

* Remove VS Code integrated terminal dependencies

* docs: sync integrated terminal removal plan status

* Remove terminal settings UI

* Remove terminal settings plumbing

* Mark terminal settings removal validated

* Remove implementation plan docs once implemented

* Polish shell integration warning UI

* Remove orphaned ACP terminal setters

* Add kanban install flow implementation plan

Start kanban install task from modal

Clarify kanban install task architecture

Verify kanban install task flow

Remove implementation plan doc once implemented

Restore direct terminal install launcher

Make the kanban installer change minimal and squashable

* Changes as per PR feedback

* Further deletions as per PR feedback

* Restore standalone kanban modal copy fallback

---------

Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-04-20 18:48:11 -07:00
Mikołaj Kondratek f6a9a02500 fix: set --max-old-space-size=8192 for cline-core node process (#10290)
* fix: set --max-old-space-size=8192 for cline-core node process

The cline-core Node.js process was launched without a V8 heap limit,
defaulting to ~2GB. Long conversations with large file reads cause
GC-thrashing and eventual OOM crashes. Set the limit to 8GB to provide
sufficient headroom for extended sessions.

* fix: set --max-old-space-size=8192 for cline-core node process
2026-04-20 09:42:08 -07:00
Tony Loehr 10af2439be docs: add prompt storage schema and OpenTelemetry events reference (#10195)
* docs: add prompt storage schema and OpenTelemetry events reference

- Add comprehensive prompt storage documentation (DEVREL-142)
  - Complete enterpriseTelemetry.promptUploading schema
  - Setup guides for AWS S3 and Cloudflare R2
  - Storage architecture and sync worker behavior
  - IAM policies and troubleshooting

- Add OpenTelemetry events catalog (DEVREL-143)
  - Document 80+ events across 8 categories
  - Example payloads and analytics query patterns
  - Integration examples for Datadog, Grafana, New Relic
  - Event schema reference and best practices

- Update monitoring documentation
  - Add cross-references between related pages
  - Update navigation in docs.json
  - Integrate new pages into Enterprise > Monitoring section

* fix: update broken link in telemetry.mdx to point to OTel events page

* docs: address PR review comments

- Fix file contents exclusion claim in prompt-storage.mdx
  - Remove misleading claim about file contents not being stored
  - Add warning that tool inputs (like write_to_file content) are included

- Standardize attribute naming in opentelemetry-events.mdx
  - Change model_id to model in event tables for consistency
  - Match actual emitted event schema shown in example payloads

- Add SQL syntax note in opentelemetry-events.mdx
  - Clarify that attribute access syntax is platform-specific
  - Provide examples for BigQuery and ClickHouse

* adjustments
2026-04-17 17:59:35 -04:00
Tony Loehr 9a8fbf9852 docs: update Anthropic docs for Claude Opus 4.7 release (#10295)
* docs: update Anthropic docs for Claude Opus 4.7 release

- Add claude-opus-4-7 and claude-opus-4-7:1m to Anthropic supported models
- Document adaptive thinking replacing classic extended thinking on Opus 4.7
- Document sampling parameter restrictions (temperature, top_p, top_k) on Opus 4.7
- Add claude-opus-4-7 to Claude Code supported models

* Update docs/provider-config/anthropic.mdx

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

* unblocker

---------

Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-17 11:51:45 -07:00
TheRealSpencer c5657a14bb chore: update axios to 1.15.0 across all packages (#10271)
- Update root package.json axios from 1.13.6 to 1.15.0
- Update evals/package.json axios from 1.13.6 to 1.15.0
- Update docs/package.json axios override from 1.13.5 to 1.15.0
- Regenerate all package-lock.json files
2026-04-17 10:52:28 -05:00
Robin Newhouse 955ae8df7a feat: wire up remote globalSkills with enterprise UI and architectural fixes [ENG-1774] (#10283)
* feat: wire up globalSkills consumption from remote config

The remote config schema already includes globalSkills (merged in #10236).
The dashboard can save skills to remote config. This PR wires up the
extension to read and use them.

## Changes

### State storage (Layer 1)
- Add remoteGlobalSkills to REMOTE_CONFIG_EXTRA_FIELDS
- Add remoteSkillsToggles to GLOBAL_STATE_FIELDS

### Remote config transform/apply/clear (Layer 2)
- Map globalSkills → remoteGlobalSkills in transformRemoteConfigToStateShape
- Sync remoteSkillsToggles in applyRemoteConfig using frontmatter.name
  as the identity key (not entry.name)
- Clear remoteSkillsToggles in clearRemoteConfig

### Skill discovery (Layer 3)
- discoverSkills accepts optional remoteSkillEntries parameter (pure
  utility, no StateManager coupling)
- getSkillContent accepts optional remoteSkillEntries parameter for
  remote content loading without disk I/O
- Precedence: remote (enterprise) > disk-global (user) > project

### refreshSkills (Layer 3b)
- Reads remote entries from controller.stateManager, parses frontmatter,
  builds SkillInfo entries with alwaysEnabled field

### UseSkillToolHandler (Layer 4)
- Toggle filter checks remoteSkillsToggles for remote: prefixed skills
- Directory note omitted for remote skills
- Passes remoteSkillEntries to both discoverSkills and getSkillContent

### toggleSkill
- Routes remote: prefixed paths to remoteSkillsToggles keyed by name

### Proto + webview
- Added always_enabled field to SkillInfo proto message
- Modal passes isRemote + alwaysEnabled to RuleRow for remote skills
- Uses skill.name as display label for remote skills

## Design decisions
- frontmatter.name is the sole identity for remote skills (entry.name
  is ignored). This matches how local skills work.
- remote: path prefix distinguishes remote from disk skills in toggle
  stores and content loading.
- skills.ts remains a pure utility module with zero StateManager coupling.
  Callers inject remote entries as parameters.
- 42 unit tests covering discovery, precedence, content loading, toggle
  sync, and frontmatter parsing.

* fix: enforce alwaysEnabled in toggle sync to prevent stale false overrides

When applyRemoteConfig syncs skill toggles, synchronizeRemoteRuleToggles
preserves existing toggle values — including false. If an admin later
sets alwaysEnabled: true on a skill that a user had previously disabled,
the stale false toggle would survive the sync. The UI would show the
skill as locked-on (via the alwaysEnabled check in refreshSkills), but
UseSkillToolHandler's filter would see false in the toggle store and
exclude it, causing a 'Skill not found' error for a skill the user can
see is active.

Fix: after synchronizeRemoteRuleToggles, force any alwaysEnabled entry
with a false toggle back to true. This makes the toggle store the single
source of truth — both UI and handler now agree.

Adds 4 tests covering the alwaysEnabled enforcement edge cases.

* fix: deduplicate remote skill parsing, add drift validation, and fix architectural gaps

1. Extract shared parseRemoteSkillEntries utility (skills.ts)
   - Single validation point for remote skill entries, replacing duplicated
     frontmatter parsing in skills.ts, refreshSkills.ts, and remote-config/utils.ts
   - Enforces entry.name === frontmatter.name to catch drift between the
     dashboard and SKILL.md content (rejects with warning on mismatch)

2. Eliminate redundant frontmatter re-parsing in getSkillContent
   - Was re-parsing every entry's frontmatter to find a match by name
   - Now uses entry.name for lookup since drift validation guarantees equality

3. Enforce alwaysEnabled in UseSkillToolHandler
   - The toggle filter was missing the alwaysEnabled check, so a stale false
     toggle could hide an admin-locked skill from the model
   - Now matches the logic in refreshSkills.ts

4. Add remote_skills_toggles to SkillsToggles proto
   - toggleSkill now returns remoteSkillsToggles in the response, matching
     how remote rules/workflows already work

5. Separate Enterprise Skills section in UI
   - Remote skills now render under their own "Enterprise Skills" header,
     consistent with how rules and workflows display remote entries

6. Update tests for new validation behavior
   - Tests now use entry.name matching frontmatter.name (was deliberately
     mismatched before); added drift rejection tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: enable viewing remote skills and fix tooltip text in RuleRow

- openRemoteFile now handles remote://skill/{name} URIs (was only
  rule and workflow), looking up content from remoteGlobalSkills
- RuleRow's handleEditClick builds the correct URI type for skills
  (was falling through to "rule")
- Tooltip text now uses ruleType ("View skill file") instead of
  hardcoded "View rule file" for all remote entries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: soften drift validation to warn-not-reject, fix content lookup fallback

The strict entry.name !== frontmatter.name rejection was silently hiding
org-configured skills when the dashboard's entry.name didn't match the
SKILL.md frontmatter name.

- parseRemoteSkillEntries now warns on drift but uses frontmatter.name as
  the canonical identity instead of rejecting the entry
- getSkillContent falls back to frontmatter match when entry.name lookup
  misses (handles drift for content loading)
- openRemoteFile falls back to frontmatter match for skill view (same
  reason)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: include remote skills in system prompt and fix remote config race

The system prompt generation called discoverSkills() without passing
remoteSkillEntries, so the model never learned about remote skills and
never invoked use_skill for them. This was the actual cause of remote
skills being invisible to the model despite showing in the UI.

Also fixes a race condition in applyRemoteConfig where clearRemoteConfig()
wiped the in-memory cache before repopulating it field-by-field. Any
concurrent reader (e.g., UseSkillToolHandler) during that window would
see an empty cache. Replaced with atomic replaceRemoteConfig() that
builds the new cache and swaps it in a single assignment.

- task/index.ts: pass remoteSkillEntries to discoverSkills, add
  remoteSkillsToggles + alwaysEnabled filtering (matching handler)
- StateManager: add replaceRemoteConfig() for atomic cache swap
- remote-config/utils.ts: use replaceRemoteConfig instead of
  clearRemoteConfig + setRemoteConfigField loop
- Remove debug logging from parseRemoteSkillEntries and handler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: include remote skills in subagent path

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:38:02 -07:00
Ara 9405419efe v3.79.0 Release Notes (#10292)
* v3.79.0 Release Notes

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

🔮 View transcript: https://agentlogs.beatrixwoo.workers.dev/s/i90f8jfxjc88hit3bs8v2e2k

* chore(cli): bump CLI to v2.15.0

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

🔮 View transcript: https://agentlogs.beatrixwoo.workers.dev/s/i90f8jfxjc88hit3bs8v2e2k

* remove changeset

* remove changeset
2026-04-16 12:26:10 -07:00
Saoud Rizwan f53dcb3096 feat(models): prepare Claude Opus 4.7 provider support (#10286)
* feat(models): prepare Claude Opus 4.7 provider support

* remove deprecated params for opus 4.7

- opus 4.7 doesn't accept params like temperature, top_p, top_k anymore.
This commit removes those params only for opus 4.7

* Agent hill climb fixes

* Anthropic adaptive thinking

* Removing 1m context switcher

* Removing 1m models fully

* Restore Anthropic 1M variants and context switchers

* Adding 1m

* remove changeset

* fix Opus 4.5 adaptive thinking detection

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Arafatkatze <arafat.da.khan@gmail.com>
2026-04-16 12:02:28 -07:00
CandiedUniverse afa32bf801 fix: Stabilize flaky Windows CI test paths (#10291)
* docs: add CI flakiness stabilization plan

* test: harden global hook cwd timeout on windows

* test: stabilize CLI skills panel interactions

* ci: harden vscode test runtime setup

* test: stabilize BannerService timer behavior

* refactor: ignore CLI skills input while loading

* docs: update stabilization plan status

* docs: drop temporary stabilization plan

* Update cli/src/components/SkillsPanelContent.tsx

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

* Update .vscode-test.mjs

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

* test: expose banner service drain hook

* fix: stabilize CLI skills panel input state

* Change polling interval

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-16 11:27:56 -07:00
Ara 2d2d9d829a fix cache reflection for cline and vercel handlers (#10266) 2026-04-15 17:47:15 -07:00
John Choi 27a1b3da8a feat: user-level remote-config discovery with inline value reuse (#10056)
Replace the old client-side per-org scan for remote config with a single
discovery call to GET /api/v1/users/me/remote-config. Reuse the inline
config value when possible, falling back to the org-level endpoint only
when inline parse fails.

Key changes:
- Single discovery call replaces N org-level requests
- Resolve config before switching org to avoid stranding the user
- Transient errors preserve existing config (log-only, no clearing)
- authenticatedRequest() strict null vs undefined validation
- Auth precheck in fetchUserRemoteConfig() with token pass-through
2026-04-15 11:21:47 -07:00
Tony Loehr f5c8cd4384 docs: fix enterprise license link to point to contact sales (#10164)
Update the Cline Enterprise License link in enterprise onboarding prerequisites from https://cline.bot/enterprise to https://cline.bot/contact-sales
2026-04-15 10:00:26 -07:00
Tony Loehr fddabb6b8d docs: add Kanban remote access documentation (#10274)
* docs: add Kanban remote access documentation

Add comprehensive documentation for accessing Kanban remotely:
- Local network access (--host flag and KANBAN_RUNTIME_HOST env var)
- Tailscale for secure remote access
- Docker deployment
- SSH tunneling
- Ngrok for public URLs
- Cloudflare Tunnels with AWS CDK example

* Update docs/kanban/remote-access.mdx

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

* Update docs/kanban/remote-access.mdx

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-04-15 09:44:55 -07:00
Robin Newhouse 071f32ec92 fix: unblock stuck command_output ask when terminal command ends (#10269)
* fix: unblock pending command_output ask on terminal completion

* test: cover timeout and idempotent command_output ask release

* refactor: simplify pending command_output ask release guard
2026-04-14 12:56:55 -07:00
KOlizer 9bdb8a9362 fix(prompts): add use_subagents to GLM, Hermes, and XS TOOL_USE_SECTI… (#10200)
* fix(prompts): add use_subagents to GLM, Hermes, and XS TOOL_USE_SECTION overrides

These variants use hardcoded TOOL_USE_SECTION templates that bypass the
auto-generated tool descriptions. When use_subagents was added as a new tool,
it was registered in each variant's .tools() config but was never added to the
hardcoded override templates — so models using these variants never saw
use_subagents in their system prompt and could not call it.

This adds the use_subagents description block to the TOOL_USE_SECTION override
templates for glm, hermes, and xs variants, and updates the corresponding
test snapshots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(prompts): gate use_subagents on subagentsEnabled and isSubagentRun context

The previous commit added use_subagents to the GLM, Hermes, and XS
TOOL_USE_SECTION override templates unconditionally. This was incorrect —
the canonical tool spec gates use_subagents with:
  context.subagentsEnabled === true && !context.isSubagentRun

Without this guard, models would advertise use_subagents even when
subagents are disabled by the user, and subagent runs could recursively
spawn further subagents.

This commit:
- Wraps the use_subagents block in all three templates with the same
  subagentsEnabled && !isSubagentRun conditional
- Converts HERMES_TOOL_USE_TEMPLATE from a plain string constant to a
  function so it can access context (matching the pattern used by GLM
  and XS templates)
- Updates snapshots accordingly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(prompts): align use_subagents rendering guard with tool context requirements

---------

Co-authored-by: sunghyun <jjinjukks1227@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 12:41:23 -07:00
Tomás Barreiro 2d994530fd Add support for Azure Blob Storage (#10264)
* Add support for Azure Blob Storage

* fix comment
2026-04-14 18:42:39 +02:00
tjandy98 e477f8fa04 Add gpt-5.2 (#10024) 2026-04-13 19:36:22 +02:00
Tomás Barreiro 36b0baec81 Add globalSkills to remote config (#10236) 2026-04-13 19:36:09 +02:00
Tomás Barreiro a0faf7c677 Fix action injection risk (#10230) 2026-04-10 23:05:06 +02:00
Tomás Barreiro 1dcf356f98 Remove old evals tool (#10226)
* Remove old evals tool

* remove script
2026-04-10 23:04:51 +02:00
CandiedUniverse 71d795eec8 Changelog and version bump for release (#10227)
* Update changelog files for release

* Version bump for release

* Update cli/CHANGELOG.md

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-04-10 12:48:17 -07:00
TheRealSpencer 792b9e89a1 update security policy to include bugcrowd vdp (#10117) 2026-04-10 13:21:35 -05:00
Roberto Langarica 10197b038d feat(chat): add SpendLimitError UI for SPEND_LIMIT_EXCEEDED (429) (#10207)
* feat(chat): add SpendLimitError UI for SPEND_LIMIT_EXCEEDED (429)

When the Cline backend returns a 429 with code SPEND_LIMIT_EXCEEDED (org
budget cap hit), the chat error flow now shows a dedicated SpendLimitError
component instead of falling through to the generic rate-limit message.

Changes:
- proto/cline/account.proto: add submitLimitIncreaseRequest RPC +
  SubmitLimitIncreaseResponse message
- src/services/error/ClineError.ts: add SpendLimit error type; detect
  SPEND_LIMIT_EXCEEDED before the generic rate-limit pattern check
- src/services/account/ClineAccountService.ts: add
  submitLimitIncreaseRequestRPC() calling POST /api/v1/users/me/budget/request
- src/core/controller/account/submitLimitIncreaseRequest.ts: new gRPC
  handler wired automatically by npm run protos
- webview-ui/src/components/chat/SpendLimitError.tsx: new card component
  mirroring CreditLimitError; shows spent/limit amounts, resets_at, org
  attribution, and a Request Increase button with 5-min localSto
When the Cline backend returns a 429 with code SPEND_LIMIT_EXCEEDED (org
budgetrors to
  SpendLbudget cap hit), the chat budget_period,limit_usd,spent_usd,resets_at}
- component instead of falling through to the generic rate-limnd Limit
  Reac
Changes:
- proto/cline/account.p

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

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

* chore: shorten spend limit error message verbiage

* fix(storybook): align spend limit story messages with component output

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-09 14:55:58 -07:00
John Choi 9390d3f933 fix: update KanbanMigrationView test to match component text (#10173)
#10161 updated the component copy but missed updating the test assertions.
This breaks CI on main and any branch based on it.
2026-04-07 16:27:38 -07:00
Saoud Rizwan 5df470bf48 fix(cli): update kanban migration view copy to not imply TUI deprecation (#10161)
The previous copy ("Cline is moving out of the terminal", "old CLI")
gave the impression that the terminal TUI was being deprecated. Updated
to frame Kanban as the new default while making clear the TUI is still
fully available.
2026-04-06 16:00:11 -07:00
Tony Loehr 034c4342d1 Complete documentation for environment variable-based OpenTelemetry configs (#10155)
* Complete documentation for environment variable-based OpenTelemetry configuration

* Address PR review comments

- Add Values column to OTLP Configuration table for consistency
- Fix New Relic endpoint to include required port 4318
- Add note about Datadog region-specific endpoints
2026-04-06 17:22:57 -04:00
Tony Loehr 349295ab2c docs: remove Teams Plan references, focus on Enterprise (#10153)
Remove Teams Plan as an option for seat upgrades in the managing-members documentation. This aligns with the product strategy to drive customers toward Enterprise for any multiplayer/team scenarios.

Related: https://github.com/cline/cline-web/pull/256
2026-04-06 11:28:50 -07:00
Tony Loehr 841402c178 docs: add all enterprise inference providers to Remote Provider Configuration (#10127)
* docs: add OpenAI Compatible and Anthropic to enterprise SaaS provider configuration

Add documentation for all inference providers supported by the core
platform's remote config system (ProviderSettingsSchema). Previously
only Bedrock, Vertex, and LiteLLM had dedicated pages.

New pages:
- OpenAI Compatible admin/member config (covers Azure Foundry)
- Anthropic admin/member config

Updated:
- overview.mdx: expanded provider table to 7 rows
- docs.json: added navigation groups
- Capabilities Manual: added provider links

* Update docs/enterprise-solutions/configuration/remote-configuration/overview.mdx

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

* Update docs/enterprise-solutions/configuration/remote-configuration/overview.mdx

Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
2026-04-06 03:51:43 +00:00
Robin Newhouse e52a052c81 Show read_file line ranges in chat UI (#10090)
Surface the actual read_file line window in chat summaries so users can see what context was added, while keeping manual approval and repeated same-file reads rendered accurately.
2026-04-02 11:26:13 -07:00
Saoud Rizwan 7a91a9be2e Bump version from 2.12.0 to 2.13.0 2026-04-01 20:09:42 -07:00
Saoud Rizwan 93a494d009 feat(cli): simplify unified update flow for cline and kanban (#10099)
* feat(cli): unify update flow for cline and kanban

* fix(cli): only install kanban when update is available

* fix(cli): include cline and kanban versions in no-update message
2026-04-01 20:08:19 -07:00
CandiedUniverse 2bd21f8a45 Update changelog and release version (#10085) 2026-04-01 10:02:58 -07:00
John Simone 2f33f71ebd Add lazy teammate mode (#10081) 2026-04-01 08:55:43 -07:00
TheRealSpencer dec10aaec3 pin axios version due to current package integrity (#10060)
* pin axios version due to current package integrity

* update axios versions
2026-03-31 11:02:20 -07:00
Dominic Cooney c09705a45f Add Linux ARM64 (aarch64) support for JetBrains plugin (#10062)
- Add linux-aarch64 to TARGET_PLATFORMS in package-standalone.mjs so
  better-sqlite3 prebuilt binaries are downloaded for this platform.
- Rename linux-arm64 to linux-aarch64 in download-ripgrep.mjs for
  consistency with the JetBrains plugin naming convention (which uses
  Java's os.arch value 'aarch64').
2026-03-30 23:57:12 -07:00
Saoud Rizwan 73058c871a ci: post release changelogs to #releases Slack channel (#10057) 2026-03-30 19:43:00 -07:00
John Choi 03211f1364 feat(sdk): add fetchFeaturebaseToken method [ENG-1673] (#10005)
* feat(sdk): add fetchFeaturebaseToken method

- Add fetchFeaturebaseToken() to ClineAccountService
- Add FeaturebaseTokenResponse type and endpoint constant
- Add E2E mock server handler and unit tests

Ref: ENG-1673

* fix: address review feedback - narrow test glob and add JSDoc

- Fix P2: scope mocharc glob to **/*.test.ts to avoid matching non-test files
- Fix P2: add JSDoc comment to fetchFeaturebaseToken for consistency

---------

Co-authored-by: John Choi <john.choi@cline.bot>
Co-authored-by: John Choi <johnwschoi@users.noreply.github.com>
2026-03-30 14:48:53 -07:00
CandiedUniverse 65e9727c65 Add is_remote_workspace metric (#10019)
* Create implementation plan doc

* feat(telemetry): track remote workspace metadata

* test(workspace): assert telemetry emission during setup

* Remove implementation plan doc

* Improvements pre- code review

* Fix as per Greptile feedback
2026-03-27 19:04:07 -07:00
Robin Newhouse 884fbfb21e feat(read_file): add chunked reading with start_line/end_line parameters (#9711)
* fix(read_file): add stable line labels in act/plan

* prompt: clarify read_file line labels for replace_in_file

* Update prompt snapshots

* feat(read_file): add chunked reading with start_line/end_line parameters

Add optional start_line and end_line parameters to read_file so models
can read files in chunks instead of loading entire files into context.
Default limit is 1000 lines per read, with a continuation hint guiding
the model to paginate when needed.

Made-with: Cursor

* feat: align read_file line format with SDK while keeping superior chunking

- Change line format from 'L1:' to '1 |' to match SDK format
- Add proper NaN validation for start_line/end_line parameters
- Keep 1000-line chunking with continuation hints (superior to SDK)
- Update tool description and tests to reflect new format
- Add directory usage guardrail back to tool description
- Update replace_in_file prompt to reference new line format

This aligns the PR with the newer SDK design patterns while
preserving the superior chunking behavior that prevents context
overflow issues.

* fix: resolve duplicate step numbering in replace_in_file instructions

Fixes the duplicate step 5 issue when NOTEBOOK_INSTRUCTIONS is
concatenated with BASE_DIFF_INSTRUCTIONS for .ipynb files.
Changes notebook instructions to step 6 to avoid ambiguity.

* test: update unit test snapshots

* test: fix gemini3 tools snapshot

* test: regenerate gemini3 tools snapshot

* fix: preserve line ranges on cached file reads

* test: refresh gemini3 tools snapshot

* test: strengthen chunked read assertions

* fix: normalize inverted read file ranges
2026-03-27 12:24:57 -07:00
Dominic Cooney 0c880c7bb1 fix: Serve H.264/MP4 for VSCode and VP9/WebM for JetBrains (#10012)
VSCode's webview doesn't reliably play VP9/WebM, while JetBrains'
JCEF lacks H.264 decoding. Restore the original H.264/MP4 video and
use HTML5 <source> elements with explicit MIME types so each platform
picks the format it supports:

  <source src=... type="video/mp4" />   <!-- VSCode -->
  <source src=... type="video/webm" />  <!-- JetBrains -->

Also:
- Restore .mp4 Git LFS tracking in .gitattributes
- Update CI LFS verification to check both files
- Add webm to JetBrains MIME type map (BrowserRequestHandler)
2026-03-27 03:35:20 -07:00
Dominic Cooney a915122c6d fix: Transcode kanban demo video from H.264/MP4 to VP9/WebM (#9997)
JetBrains IDEs use JCEF (Chromium Embedded Framework) for webviews,
which often does not include H.264 decoding due to licensing
restrictions. This causes the kanban demo video to fail to play.

Transcode the video from H.264/MP4 to VP9/WebM, which is a royalty-free
codec universally supported in Chromium derivatives. This also reduces
the file size from 3.6MB to ~900KB.

Updated all references:
- Source component import (ClineKanbanLaunchModal.tsx)
- Git LFS tracking (.gitattributes)
- CI workflow LFS checks (publish.yml, publish-nightly.yml)
2026-03-27 07:39:05 +09:00
Robin Newhouse 835ed94736 fix: exclude new_task tool from system prompt in yolo/headless mode (#9958) 2026-03-26 12:10:47 -07:00
Robin Newhouse 00ca3d13fa test(oca): add messages api routing and stream helper tests (#9494) 2026-03-26 12:02:13 -07:00
CandiedUniverse 3f3a87aed9 Hooks: Notification hook polish (#9909)
* Create implementation plan doc

* Implement notification hook helper

* Remove generated proto artifacts from branch

* Remove implementation plan doc

* Update src/core/hooks/__tests__/notification-hook.test.ts

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

* Update src/core/hooks/notification-hook.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>
Co-authored-by: Tony Loehr <turingxo@gmail.com>
2026-03-26 10:10:15 -07:00
John Simone 7a0f11837e Add Kanban docs (#9988)
* add kanban docs

* fix rendering error in overview, drop kanban flags from commands

* fix stray link

---------

Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
2026-03-26 08:12:09 -07:00
Max 03d2d01eed chore: bump version to 3.76.0 (#9986) 2026-03-26 07:52:28 -07:00
Dominic Cooney db1b1c45bd chore: update dependencies flagged by npm audit (#9980) 2026-03-26 21:03:35 +09:00
Saoud Rizwan a4131e57d8 fix(cli): replace --tui hint with kanban description in migration view
The migration announcement didn't explain what Kanban is. Swap the
"run cline --tui" line for a one-liner describing the product so users
know what they're opting into. The --tui escape hatch is still
discoverable via the Exit menu item.
2026-03-25 22:52:22 -07:00
Saoud Rizwan 683dd9a741 chore(cli): bump version to 2.11.0 2026-03-25 19:24:48 -07:00
Saoud Rizwan 0d437f71b0 fix(cli): remove noisy install message when launching kanban 2026-03-25 19:24:20 -07:00
Saoud Rizwan fc77c0faea chore(cli): bump version to 2.10.0 2026-03-25 18:38:24 -07:00
Saoud Rizwan 12c85c4233 Update tips about kanban 2026-03-25 18:26:06 -07:00
Saoud Rizwan d6b7a1ab41 fix(cli): launch kanban directly with package manager fallback 2026-03-25 18:26:06 -07:00
CandiedUniverse 3926e7b404 Stabilize auth e2e onboarding tests and clean up mock server URL parsing (#9974)
* Fix tests that fail when kanban modal is visible

* Fix deprecation warning

* Improve isVisible() as per Greptile suggestion
2026-03-25 16:35:09 -07:00
Tony Loehr 5ba4314b9a feat: add toggle to disable feature tips in chat (#9973) 2026-03-25 15:28:09 -07:00
John Choi 9ccad7f764 refactor: replace hand-rolled YAML parser in refreshSkills with shared helper (#9956)
* refactor: replace hand-rolled YAML parser in refreshSkills with shared helper

The refreshSkills controller had its own line-by-line YAML parser that
only handled simple key: value pairs. Replace it with the shared
parseYamlFrontmatter helper already used by the skills discovery path.

Same output for name and description fields. The shared parser handles
edge cases (arrays, nested values, quoted colons) more robustly.

* refactor: inline parseYamlFrontmatter, remove redundant wrapper

Remove the parseFrontmatter wrapper since only `data` is used by
the caller. Inline the call directly at the use site.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: John Choi <johnchoi@MacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:06:15 -07:00
Saoud Rizwan 3562f54dbf fix(cli): enable shell mode for kanban spawn on Windows
npx.cmd requires shell: true on Windows to resolve correctly.
2026-03-25 07:35:53 -07:00
Octopus d427d5d76a feat: upgrade MiniMax default model to M2.7 (#9886)
* feat: upgrade MiniMax default model to M2.7

- Add MiniMax-M2.7 and MiniMax-M2.7-highspeed to model list
- Set MiniMax-M2.7 as default model
- Keep all previous models as alternatives
- Update provider documentation

* fix: correct M2.7 cache pricing and update prompt caching docs

- Updated cacheReadsPrice from 0.015 to 0.03 for both MiniMax-M2.7 and
  MiniMax-M2.7-highspeed to match M2.5 pricing (same cache read rate
  across M2.x models)
- Updated prompt caching tip to explicitly mention highspeed variants

* fix: correct MiniMax model pricing to match official rates

- M2.7-highspeed/M2.5-highspeed/M2.1-lightning: $0.60/$2.40 (not $0.30/$1.20)
- M2.7 cache: reads $0.06/M (not $0.03), writes $0.375/M (not $0.0375)
- All models: cache writes $0.375/M (not $0.0375)

Ref: https://platform.minimax.io/docs/llms.txt

---------

Co-authored-by: PR Bot <pr-bot@minimaxi.com>
2026-03-25 05:17:13 -07:00
Saoud Rizwan 072e2887b0 fix(cli): add --tui flag to TUI e2e tests to bypass Kanban redirect
The Kanban launch-by-default feature (beb54a4) redirects bare `cline`
invocations to Kanban, which blocks all TUI tests that launch without
`--tui`. Adding the flag ensures tests reach the legacy TUI as expected.
2026-03-25 04:36:12 -07:00
Saoud Rizwan 9c71a6f021 feat(webview): add Cline Kanban launch modal and queue announcements (#9963)
* feat(webview): add dedicated Cline Kanban launch modal

* fix(ci): fetch LFS media assets in publish workflows
2026-03-25 04:32:09 -07:00
Saoud Rizwan 7627a382aa feat(cli): launch kanban by default with migration view (#9914)
* feat(cli): refresh welcome banner and kanban launcher

* feat(cli): launch kanban by default with migration view

* feat(cli): add kanban process lifecycle management

Detach the kanban child process on Unix so it gets its own process
group, forward signals to the group for graceful shutdown, and fall
back to SIGKILL after a 10s timeout. Resolves exit codes from signals
correctly (130 for SIGINT, 143 for SIGTERM).
2026-03-25 02:01:56 -07:00
Tomás Barreiro 978c633c90 Add KanBan to remote config [PF-632] (#9959)
* Add KanBan to remote config

* Update tests
2026-03-25 02:33:52 +01:00
Tony Loehr e807b520e0 feat(cli): add rotating feature tips during thinking/acting phases (#9874)
* feat(cli): add rotating feature tips during thinking/acting phases

Port the FeatureTip component from the VSCode extension (PR #9799) to the
CLI. Shows rotating educational tips below the ThinkingIndicator while Cline
is processing, appearing after a 2-second delay and cycling every 8 seconds.

Includes 17 tips covering:
- Core features: .clinerules, Plan/Act mode, checkpoints, MCP servers
- CLI-specific: /settings, /skills, /history, /compact, /reportbug
- Workflows: kanban, auto-approve toggle, history navigation, @ mentions
- Images via --images flag, browser testing, double-check completion

* Update FeatureTip.tsx

* Apply suggestions from code review

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

* fix: resolve FeatureTip.tsx syntax errors (duplicate export, missing tipIndex state)

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-24 17:57:35 -07:00
John Choi f3a3f30db5 feat: add repeated tool call loop detection (#9933)
* feat: add repeated tool call loop detection

Detect when the LLM calls the same tool with identical arguments
repeatedly, which wastes tokens without making progress. This is
the #1 active complaint in the Cline issue tracker (13+ open issues
including #9923, #9916, #9846, #9816).

Two-stage escalation:
- Stage 1 (3 identical calls): inject a warning nudging the LLM
  to try a different approach
- Stage 2 (5 identical calls): trigger the existing
  consecutiveMistakeCount escalation (asks user or fails in YOLO)

Detection uses JSON.stringify with a sorted key replacer for
deterministic comparison. Metadata params like task_progress
(which change every call even when actual tool arguments are
identical) are stripped from the comparison.

Complementary to fileReadCache, which deduplicates file content but
still allows the tool call to succeed and consume a turn. Loop
detection catches the repeated call pattern itself.

Changes:
- loop-detection.ts: shared helper (toolCallSignature, checkRepeatedToolCall)
- TaskState.ts: add lastToolParams, consecutiveIdenticalToolCount
- ToolExecutor.ts: call checkRepeatedToolCall in handleCompleteBlock
- responses.ts: add formatResponse.repeatedToolCall
- loop-detection.test.ts: 5 tests

Manually verified: CLI test confirms soft warning at call 3,
YOLO mode failure at call 5.

Refs: #9923, #9916, #9846, #9816

* fix: address review feedback on loop detection

- Change hardEscalation threshold from >= to === so it fires exactly
  once at count 5, matching softWarning behavior
- Widen toolCallSignature param type to Partial<Record<string, string>>
  to match actual block.params type from ToolExecutor
- Add negative boundary assertions to verify no false positives at
  calls 0, 1, 3, 4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reset loop detection state when user continues after mistake_limit_reached

When hard escalation fired at count === 5 and the user clicked
"continue", consecutiveIdenticalToolCount was never reset. The count
would exceed 5 but === 5 never matched again, silently disabling
loop detection for the rest of the task.

Reset consecutiveIdenticalToolCount, lastToolName, and lastToolParams
alongside the existing consecutiveMistakeCount reset so the detector
fully re-arms after the user continues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: John Choi <johnchoi@MacBook-Pro.local>
2026-03-24 13:40:31 -07:00
Igor Tceglevskii f1c7934064 fix: prevent OOM crash from globby's eager .gitignore scanning in lis… (#9917)
* fix: prevent OOM crash from globby's eager .gitignore scanning in listFiles

Replace globby's gitignore:true (which reads ALL .gitignore files in the
entire tree upfront, including inside gitignored directories) with
incremental .gitignore reading during BFS traversal.

In projects with large gitignored vendored dependencies containing many
nested repos, globby collects thousands of patterns, builds a massive
regex, and V8 runs out of memory during regex compilation (~488MB).

The fix reads .gitignore files only from directories the BFS actually
enters. Gitignored directories are never entered, so their .gitignore
files are never parsed and the pattern count stays small.

- Set gitignore:false, handle .gitignore ourselves
- Read root .gitignore in buildIgnorePatterns() to seed initial patterns
- Read subdirectory .gitignore lazily during globbyLevelByLevel BFS
- Accumulate patterns in currentIgnore so deeper levels respect them
- Add 4 tests: root patterns, file patterns, subdirectory .gitignore,
  and OOM-prevention (no reading inside gitignored dirs)

* Code review followup

* Potential fix for code scanning alert no. 147: Incomplete string escaping or encoding

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-22 15:20:57 -07:00
Robin Newhouse 1d42da5248 Add tool-specific error messages for replace_in_file and execute_command (#9710)
Replace generic "missing parameter" errors with targeted guidance for
the two tools observed failing most in SWE-bench (6% of failures).
The new messages include the expected format (SEARCH/REPLACE blocks,
XML example) without the 30-line boilerplate reminder.

Made-with: Cursor
2026-03-21 11:57:00 -07:00
Ara 7f3974a827 fix: read cache_write_tokens from OpenRouter API instead of hardcoding 0 (#9871)
* fix: read cache_write_tokens from OpenRouter API instead of hardcoding 0

- Read prompt_tokens_details.cache_write_tokens from OpenRouter stream usage
  chunks instead of hardcoding cacheWriteTokens to 0
- Read native_tokens_cache_write from generation endpoint fallback
- Replace fragile hardcoded model ID switch statement for cache_control blocks
  with prefix-based matching (anthropic/, minimax/) so new models automatically
  get prompt caching enabled
- Add unit test verifying cache_write_tokens are correctly extracted

Co-authored-by: Ara <arafat.da.khan@gmail.com>

* fix: read cache_write_tokens from OpenRouter API instead of hardcoding 0

- Read prompt_tokens_details.cache_write_tokens from OpenRouter stream usage
  chunks instead of hardcoding cacheWriteTokens to 0
- Read native_tokens_cache_write from generation endpoint fallback
- Replace fragile hardcoded model ID switch statement for cache_control blocks
  with prefix-based matching (anthropic/, minimax/) so new models automatically
  get prompt caching enabled
- Add unit test verifying cache_write_tokens are correctly extracted

Co-authored-by: Ara <arafat.da.khan@gmail.com>

* Release v3.74.0 Notes

* Release v3.74.0 Notes

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: alex-lum <alex@cline.bot>
2026-03-20 15:46:29 -07:00
CandiedUniverse d40ab56aff Changelog and version bump for release (#9910) 2026-03-20 15:29:45 -07:00
CandiedUniverse bbdf445db7 fix: prevent resume asks from unblocking on abort (#9908)
* fix: prevent resume asks from unblocking on abort

* Add test case for resume_completed_task

* Prevent flaky tests
2026-03-20 10:51:16 -07:00
CandiedUniverse d992a3bf21 Stabilize hooks fixture tests with per-scenario isolation (fix flaking tests) (#9868)
* Refactor flaking hooks tests

Fix flaking hooks tests

Strengthen hooks fixture test isolation

* Address Greptile hooks test review

* Harden Windows hook timing tests

* Loosen hook cancellation timing margins

* Fixing more flaking tests
2026-03-20 09:50:14 -07:00
Max ace95988f8 fix: resolve deadlock when clicking New Task/Exit after task completion (#9905)
The presentation scheduler introduced in ff05ec3bb awaits in-flight
flushes during dispose(), but ask() blocks indefinitely on pWaitFor
waiting for user input. When abortTask() sets abort=true and then
awaits scheduler.dispose(), the in-flight flush (blocked on
ask("completion_result")) never resolves, deadlocking the UI.

Add abort flag check to ask()'s pWaitFor condition so blocked asks
unblock immediately when the task is aborted.

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 08:53:21 -07:00
Tony Loehr 54726f1677 docs: fix SDK documentation accuracy and completeness (#9856)
* docs: fix SDK documentation accuracy and completeness

- Fix setPermissionHandler API Reference to use correct async/return signature
  (was showing old callback pattern with (request, resolve) => void)
- Remove non-existent PermissionResolver type from Exported Types table
- Fix PermissionHandler type description to match actual signature
- Add missing hooksDir option to ClineAgentOptions documentation
- Fix newSession() example to use real model IDs
- Replace developer personal path in Full Example with generic path
- Use placeholder for version in initialize() example to avoid staleness
- Expand Stop Reasons table and add note about current implementation
- Add missing key exported types: AcpSessionStatus, AcpSessionState,
  RequestPermissionRequest/Response, PermissionOption, SessionUpdatePayload,
  SessionModelState, ModelInfo, TextContent/ImageContent/AudioContent,
  SetSessionMode/Model request/response types, TranslatedMessage

* docs: improve SDK visibility and disambiguate from API code examples

- Move SDK page higher in Cline CLI nav (after Installation, before Interactive Mode)
- Add sidebarTitle 'SDK (Programmatic Use)' for clearer nav label
- Rename api/sdk-examples to 'Code Examples' to avoid naming confusion with the Cline SDK
- Update API overview card title to match

* Apply suggestions from code review

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

* docs: add ClientCapabilities, Error Handling, and BYO API key docs to SDK

- Document clientCapabilities object and its effect on agent behavior
- Add Error Handling section with all throwable errors per method
- Expand BYO API key setup with concrete CLI auth examples

* docs: fix duplicated Stop Reasons table rows from code review

* docs: fix 3 accuracy issues found in source code audit

- Fix protocolVersion: was '0.9.0' (fabricated), actually 1 (number) from @agentclientprotocol/sdk
- Fix clientCapabilities: was claiming they change SDK behavior, but ClineAgent always uses standalone providers (capabilities only matter via AcpAgent stdio wrapper)
- Fix permission options: remove reject_always (never sent by agent, only allow_once/allow_always/reject_once are used)

* docs: clarify custom clineDir usage with CLI --config flag

Address PR review feedback: the BYO auth section mentioned custom
clineDir without showing how to target it from the CLI. Remove the
vague reference and add explicit --config flag documentation with
side-by-side SDK and CLI examples.

* docs: remove misleading 'by default' qualifier from SDK BYO auth section

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-19 21:01:24 -07:00
Saoud Rizwan 6308fef0a9 fix(cli): use kanban@latest to always fetch newest version (#9898)
* fix(cli): use kanban@latest to always fetch newest version

npx -y kanban may use a cached version. Using @latest ensures
users always get the most recent kanban release.

* chore(cli): bump version to 2.8.2 and update changelog
2026-03-19 19:37:53 -07:00
CandiedUniverse b3fc79b8ce Remove example hooks (#9896) 2026-03-19 18:42:55 -07:00
CandiedUniverse ff05ec3bbe Latency improvements for remote workspaces (#9858)
* Implement minimal change set for latency improvements

Cline's code review improvements

Further code review improvements

* fix: address code review issues for presentation scheduler

- Add reset() method to TaskPresentationScheduler to prevent stale
  timer leaks between API request retries within the same task
- Call presentationScheduler.reset() in streaming state reset section
- Fix hadVisibleAssistantContent to track reasoning content, not just
  text, preventing every reasoning chunk from getting immediate priority
- Remove unused 'low' priority from PresentationPriority type and
  associated cadence configuration (YAGNI)
- Trim TaskLatencyTrigger to only values actually used: text/reasoning/tool
- Add JSDoc to isRemoteWorkspaceEnvironment documenting the heuristic
  fallback and its false-positive risk
- Add Logger.warn for invalid cadence env-var overrides
- Add comment at scheduler construction explaining detection promise
  dependency on remoteWorkspaceDetectionPromise
- Add comments to CLI host bridge about intentional remoteName omission
- Expand test coverage: reset(), coalescing, priority upgrade tests

* fix: address code review issues in latency/presentation scheduler

- Fix #1/#3: Set didPresentAnyContent=true for text and tool_calls chunks
  so coalescing actually activates for text-only and tool streams (was
  only set for reasoning chunks, defeating the 40/90ms cadence goal)

- Fix #2: Re-check flushInProgress after awaiting in-flight flush in
  runFlushCycle to prevent two concurrent callers from both proceeding
  past the guard and starting concurrent flushes

- Fix #4: Document flushNow() disposed no-op contract so callers
  understand the finalization guarantee

- Fix #5: Add disposed guard to reset() to prevent post-dispose state
  mutation

- Fix #6: Remove platform/version heuristic from isRemoteWorkspaceEnvironment
  — only use remoteName. The substring match on 'remote' produced false
  positives for version strings like '1.0.0-remote-fix'. Non-VSCode hosts
  should populate remoteName explicitly to opt in to the higher cadence.

- Fix #7: Add remoteWorkspaceDetectionSettled flag and warn if getDelayMs
  is called before detection resolves, making the timing dependency
  explicit and detectable in production logs

- Fix #8: Upgrade disabled-scheduler bypass error from Logger.debug to
  Logger.warn so silent flush failures are visible

- Update latency.test.ts: replace heuristic test with false-positive
  regression tests and add null/empty-field coverage

* fix: address code review issues in latency/presentation scheduler

- Fix flushNow race condition: wait for all in-flight flushes to drain
  before setting pendingPriority so the post-flush continuation cannot
  steal it, guaranteeing at least one flush runs after flushNow() returns
- Add regression test for the flushNow race condition
- Fix disabled-path (CLINE_DISABLE_PRESENTATION_SCHEDULER): use
  presentationScheduler.flushNow() instead of a fire-and-forget void
  call so the presentAssistantMessage lock/pending-updates mechanism
  is respected when the scheduler is bypassed
- Rename didPresentAnyContent -> didScheduleAnyContent to accurately
  reflect that content has been scheduled, not necessarily flushed
- Update stale comment in streaming loop to match new variable semantics
- Upgrade remote workspace detection failure log level from debug to warn
- Document reset() interaction with in-flight flush on already-reset state
- Document empty-string remoteName edge case in getHostVersion.ts

* refactor: polish presentation scheduler for production readiness

- Extract PresentationPriority type to shared presentation-types.ts,
  breaking the latency.ts → TaskPresentationScheduler.ts type coupling
- Remove redundant pendingWhileFlushing flag from TaskPresentationScheduler;
  pendingPriority alone is sufficient for the post-flush continuation check
- Cache env var reads in latency.ts at module load (hot path optimization)
- Inline flushAssistantPresentation() one-liner into the scheduler constructor
- Make processNativeToolCalls protected to enable type-safe test access
- Remove as any cast in Task.processNativeToolCalls.test.ts

* Fixes as per Greptile review feedback

* Further fixes as per Greptile feedback

* Further fixes as per Greptile feedback
2026-03-19 17:05:49 -07:00
Ara bde7049c01 Release v3.74.0 Notes (#9879) 2026-03-18 19:08:50 -07:00
Ara 7cd06744ad feat: implement dynamic free model detection for Cline API (#9878)
Replace hardcoded free models list with runtime resolution from
recommended models. The handler now dynamically fetches free model
IDs using refreshClineRecommendedModels with fallback to static
defaults, and normalizes model IDs for consistent comparison.
2026-03-18 18:55:06 -07:00
shey-cline c88d3238cf Replace Error Message When Not Logged In to Cline (#9632) 2026-03-18 16:01:06 -07:00
jessy-cline 852ca2348f fix(ui): align ClineRulesToggleModal padding with ServersToggleModal (#9870)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-03-18 14:13:39 -07:00
CandiedUniverse 7091ccf2c7 Fix flaky CLI Enter-key handling across Windows/test environments (#9867)
* fix(cli): normalize enter key handling in ink inputs

* fix(cli): pass input through auth select enter handling
2026-03-18 12:20:25 -07:00
Mathis ad6c33ac5b feat: add file read deduplication cache to prevent repeated reads (#9836)
* feat: add file read deduplication cache to prevent repeated reads

- Add fileReadCache to TaskState for tracking read files per task
- ReadFileToolHandler checks cache before reading, returns cached content on repeat reads
- Warns model after 3+ reads of same file to stop re-reading
- WriteToFileToolHandler and ApplyPatchHandler invalidate cache on file changes
- Reduces wasted API tokens from models reading same files repeatedly

* fix: address file read cache gaps - image blocks, execute_command, redundant invalidation

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

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

* PR changes

- __`TaskState.ts`__ — Simplified cache type from `{ content: string; readCount: number; imageBlock? }` to `{ readCount: number; mtime: number; imageBlock? }`. Dropped `content` to save memory; added `mtime` for external change detection.

- __`ReadFileToolHandler.ts`__ — Four improvements:

  - __Removed redundant `.set()` call__ — reviewer was correct that objects are modified by reference in Map
  - __Added mtime-based cache validation__ — on cache hit, `stat()` the file and compare mtime. If the file was modified externally (user edited in their editor), the cache entry is evicted and a fresh read occurs
  - __Dropped content from cache__ — cache now only stores metadata (readCount, mtime, imageBlock). On cache hits, the file is re-read from disk, addressing memory concerns
  - __Softened readCount >= 3 warning__ — removed the aggressive "Do NOT read this file again" language; now says "Please use the information you already have and proceed with your task"

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-18 10:38:23 -07:00
Tony Loehr 71e312e92a feat: add feature tips tooltip during thinking state (#9799)
* feat: add feature tips tooltip during thinking state

Show rotating feature tips below the Thinking indicator to keep users engaged and educate them on features like Double-Check Completion, .clinerules, Plan Mode, MCP Servers, checkpoints, and more.

- New FeatureTip component with 12 curated tips
- 2s delayed appearance, 8s cycling with fade transitions
- Visible throughout entire thinking/reasoning phase
- Proper timer cleanup on unmount

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

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

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

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

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

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

* fix: remove unused useMemo import from FeatureTip

* fix: increase test delays in QuitCommand.test.tsx for Windows CI reliability

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

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

* fix: add smooth fade-in for first tooltip appearance

The first tooltip was appearing abruptly because the element went from
not-in-DOM (return null) to opacity-100 instantly. Added hasFadedIn state
with requestAnimationFrame to ensure the CSS transition applies on initial
render, giving the first tip a smooth 300ms fade-in.

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-17 13:51:37 -07:00
Charlotte Stinson 5903840f79 Added Fields to Responses API for the Oracle Code Assist (OCA) Provider (#9852)
* added fields to responses api

* fix temp problem

* cleaning up

* clean modelId

* Apply suggestion from @greptile-apps[bot]

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

---------

Co-authored-by: Charlotte Stinson <charlottestinson@Charlottes-MacBook-Pro.local>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-17 13:41:39 -07:00
ClineXDiego 0c677b63db fix(browser): skip WebP for GLM and Devstral models running through llama.cpp (#9837)
llama.cpp's STB image library doesn't support WebP format. Users running
GLM 4.6V, GLM 4.5, and Devstral models via llama.cpp server (openai-compatible
endpoint) were hitting a 400 error when using the browser tool because Cline
sends screenshots as WebP by default.

modelDoesntSupportWebp() only checked for Grok models. Extend it to also
cover GLM and Devstral model families using the existing family detection
functions. Also update isGLMModelFamily() to handle space-separated model IDs
like 'GLM 4.6V' (the format llama.cpp server reports for this model).

Fixes #8203
2026-03-17 14:57:40 -03:00
ClineXDiego b741135f85 fix(litellm): respect user-configured context window in getModel() (#9834) 2026-03-17 05:36:44 -07:00
Ara 2eab216815 fix(wandb): honor explicit model IDs outside static catalog (#9839) 2026-03-16 16:40:28 -07:00
Ara e57174eec8 fix(fireworks): add missing serverless models and pricing (#9810)
* feat: refresh Fireworks serverless model defaults

* fix(fireworks): handle cached token usage and write pricing

* fixing maxtokens for gemini family

* feat(fireworks): add missing serverless models and pricing
2026-03-16 14:28:23 -07:00
Max ad4631c682 bump cline version (#9833) 2026-03-16 13:36:41 -07:00
Mathis aae23e8685 fix: Claude Code provider failing with 4.6 models and newer CLI versions (#9783)
* fix: Claude Code provider failing with 4.6 models and newer CLI versions

- Update --disallowedTools list to match current Claude Code CLI tools
  (12 new tools were unblocked, causing models to use native tool_use
  instead of Cline's XML tools)
- Fix rate_limit_event handling for new CLI format (top-level type
  instead of system subtype)
- Handle unknown content block types and new message types gracefully
- Fix assistantHasContent check to account for tool calls accumulated
  via toolUseHandler even when useNativeToolCalls is false

* resolved .include mismatch to .containEql

* Update src/integrations/claude-code/types.ts

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

* Removed `LegacyRateLimitEvent` type and its union reference from `types.ts`

* Fixed loop with async tool calls in new claude code.

* PR review feedback fixes

__Fix #2 (claude-code.ts):__ Cleaned up the error field check — replaced verbose `"error" in message` guard + ternary chain with a simpler `message.error` check using optional chaining and nullish coalescing (`message.content?.[0]` + `?? fallback`).

__Fix #3 (claude-code.ts):__ Replaced `message.content.length > 0 ? message.content[0] : undefined` with `message.content?.[0]` using optional chaining for the `stop_reason` block.

__Fix #4 (claude-code.ts):__ Replaced repeated `(content as any)` casts in the `default` switch case with a single typed cast: `const unknownBlock = content as { type: string; text?: string }`, making the code cleaner and safer.

__Fix #5 (ApplyPatchHandler.ts):__ Replaced both `await import("node:path")` and `require("node:path")` dynamic imports with a static `import { resolve as resolvePath } from "node:path"` at the top of the file.

* Remove file read deduplication feature (moved to separate PR)

* Remove ReadFileToolHandler file-not-found test (moved to separate PR)

* Add LegacyRateLimitEvent type for older CLI format

* Restore ReadFileToolHandler.ts and test from upstream/main (fix stale local main revert)

* Revert ReadFileToolHandler.ts to match fork main (no try/catch, no test file)

* manually reverting back

* Update src/core/api/providers/claude-code.ts

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

* Restore ReadFileToolHandler.ts and test to match cline/cline upstream main

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-16 13:01:55 -07:00
dependabot[bot] 0ff27591d5 chore(deps-dev): bump lodash (#8803)
Bumps the npm_and_yarn group with 1 update in the /testing-platform directory: [lodash](https://github.com/lodash/lodash).


Updates `lodash` from 4.17.21 to 4.17.23
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 12:25:46 -07:00
dependabot[bot] 23127f22de chore(deps): bump qs from 6.14.1 to 6.15.0 (#9343)
Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.15.0.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.1...v6.15.0)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.15.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 12:00:02 -07:00
CandiedUniverse 57ed14d1b1 Fix issue with Windows notification (#9743)
* Fix issue with Windows notification

Fix Windows proto tooling

Fix Windows unit test path normalization

Revert "Fix Windows unit test path normalization"

This reverts commit 73400a3ca6f0300d009f7c8238a016d769186f3a.

Remove package-lock.json changes

* Remove unnecessary changes

* Use command approval string for notifications

* Address PR feedback on Windows notifications

* Fix Windows protoc path for CI

* Polish notification safety and test coverage

* Fix unfound tests in CI

* Harden Windows notifications and protoc execution

* Fix Windows path normalization in glob test

* Fix as per Greptile feedback
2026-03-16 09:44:20 -07:00
aikido-autofix[bot] a769585406 fix(security): update dependencies (#9812)
Co-authored-by: aikido-autofix[bot] <119856028+aikido-autofix[bot]@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
2026-03-16 09:15:40 -07:00
CandiedUniverse c44b29b002 Fix Windows CLI tests related to /q and /exit (#9747)
* Fix flaky Windows CLI quit slash tests

* Refine CLI slash command handling and test stability

* Address Greptile cleanup feedback
2026-03-16 07:59:26 -07:00
dependabot[bot] bb4e397a51 chore(deps): bump undici from 7.20.0 to 7.24.3 (#9825)
Bumps [undici](https://github.com/nodejs/undici) from 7.20.0 to 7.24.3.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.20.0...v7.24.3)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.24.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 07:52:50 -07:00
Saoud Rizwan 9824d8d476 Bump CLI version from 2.7.0 to 2.7.1 2026-03-15 20:36:02 -07:00
Saoud Rizwan a46c5288ca Fix Notification hook getting called by command output asks 2026-03-15 20:08:06 -07:00
Mohammad Bakir 91b947de69 feat: Add W&B Inference by Coreweave as provider (#9800)
* feat(wandb): add W&B Inference by CoreWeave provider

Adds support for W&B Inference as an API provider using a W&B API key.
Implements a provider handler with OpenAI-compatible streaming and a static
model catalog, and wires the provider through the API layer, configuration
schema, storage, CLI model picker, and settings UI.

* Updated input/output price of NVIDIA-Nemotron

* Updated helpText

* handle reasoning tokens in streaming respons

* Added clarifying comment on how W&B token usage is reported and why cached tokens

* fix: restore proto field numbers changed by generation script

* Update src/core/api/providers/wandb.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-03-13 16:06:35 -07:00
Saoud Rizwan 7b25a21b26 fix(cli): add -y flag to npx kanban to auto-confirm install 2026-03-13 15:47:18 -07:00
Ara 1d1071dcf5 fix: consolidate Parallel tool-calling fixes (#9738)
* fix: consolidate parallel tool-calling fixes

* test(snapshot): fix vertex gemini3 snapshot newline

* fix gemini toolcall id collision (#9768)

* test(snapshot): fix vertex gemini3 snapshot newline

* fix(gemini): prevent native tool-call ID collisions

* fixing maxtokens for gemini family

* test(gemini): assert fallback tool call ids
2026-03-13 09:20:23 -07:00
Dominic Cooney 8d07b7d6cb fix: catch errors in path-based tool handlers instead of crashing (#9732)
* fix: catch errors in path-based tool handlers instead of crashing

ListCodeDefinitionNamesToolHandler, ListFilesToolHandler, and
SearchFilesToolHandler let exceptions from their core operations
propagate through ToolExecutor's re-throw path, crashing the CLI
process. This is the same class of bug fixed for ReadFileToolHandler
in #9730.

Changes per handler:
- Wrap the core operation in try/catch, returning formatResponse.toolError()
  on failure so the model can see the error and recover gracefully.
- Move consecutiveMistakeCount reset to after a successful operation so
  repeated failures accumulate toward the yolo-mode mistake limit.
- Increment consecutiveMistakeCount on caught errors.

Add end-to-end tests exercising each handler with a mock TaskConfig,
covering: non-existent paths, missing parameters, failure accumulation,
and success-based counter reset.

* address review: expand try/catch scope, add stub-based tests

- Include resolveWorkspacePath inside try/catch in
  ListCodeDefinitionNamesToolHandler and ListFilesToolHandler (matching
  SearchFilesToolHandler's pattern) so path resolution failures are
  also caught gracefully.
- Fix trivially-true assertion in file-not-a-directory test.
- Add 6 new stub-based tests that force core operations to throw:
  parseSourceCodeForDefinitionsTopLevel, listFiles, and
  determineSearchPaths — verifying the catch paths return
  formatResponse.toolError() and increment consecutiveMistakeCount.
- Total: 19 passing tests (up from 13).

* address review: move clineignore check before IO in ListFilesToolHandler

Move the .clineignore access validation before resolveWorkspacePath and
listFiles so blocked paths are rejected without incurring IO cost.
Also ensures consecutiveMistakeCount is only reset after all
validations and the core operation succeed.

* address review: increment counter on clineignore denial

Clineignore denial in ListFilesToolHandler now increments
consecutiveMistakeCount so repeated attempts at blocked paths
accumulate toward the yolo-mode mistake limit. Added 2 tests
verifying single and repeated clineignore denials.
Total: 21 passing tests.

* fix: increment consecutiveMistakeCount when SearchFilesToolHandler searches fail

Previously, SearchFilesToolHandler's executeSearch() caught regexSearchFiles
errors and returned {success: false}, but the handler unconditionally reset
consecutiveMistakeCount to 0 even when ALL searches failed. This contradicted
the PR's goal of accumulating failures toward the yolo-mode mistake limit.

Now we check if any search succeeded before resetting the counter:
- If at least one search succeeded: reset to 0 (existing behavior for successes)
- If all searches failed: increment the counter (new fix)

Also added comprehensive test coverage for this scenario, including tests for:
- regexSearchFiles throwing errors
- Repeated search failures accumulating
- Successful search resetting the counter after failures

* fix: detect error strings in ListCodeDefinitionNamesToolHandler

parseSourceCodeForDefinitionsTopLevel returns error strings instead of
throwing exceptions for file paths and non-existent directories. The
handler now detects these error conditions and increments
consecutiveMistakeCount so repeated failures accumulate correctly.

This addresses Greptile's feedback that the counter was unconditionally
resetting to 0 for all real-world failure modes of this handler.
2026-03-12 20:58:06 -07:00
Dominic Cooney aacc69a558 fix: catch file-not-found in ReadFileToolHandler instead of crashing CLI (#9730)
* fix: catch extractFileContent errors in ReadFileToolHandler

When extractFileContent throws (e.g. file not found), the exception
propagated through ToolExecutor which re-threw it, crashing the CLI
process with exit code 1.

Now file read errors are caught and returned as formatResponse.toolError()
so the model can see the error and recover gracefully (e.g. try a
different file path) instead of terminating the entire task.

Also increments consecutiveMistakeCount so the yolo-mode mistake limit
still functions correctly.
2026-03-13 10:52:32 +09:00
Max a27bedffb6 max/cli tui e2e tests (#9753)
* add tui UI tests

using microsoft/tui-test library, can run many headless versions of
cline and execute ui tests (requires Node <= 20)

improve brittle sleep calls

* add cli-tui-tests github action

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-03-12 16:06:25 -07:00
Diego Ferreyra bbeecefe84 docs: fix broken Oracle Code Assist website link (#9782)
The Oracle Code Assist URL moved from /artificial-intelligence/code-assist/
to /application-development/code-assist/. The old URL returns a 404.

Fixes #9776

Co-authored-by: gatof81 <gatof81@users.noreply.github.com>
2026-03-12 11:56:34 -03:00
alex-lum 50b57f472f feat(telemetry): add provider to task.tokens event (#9762) 2026-03-11 17:30:03 -07:00
CandiedUniverse 0e7e0099cd Changelog and version bump for release (#9775) 2026-03-11 17:21:04 -07:00
AJ Juaire 44cbfe9d9c Update Jupyter Notebook gifs (#9772)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-03-11 16:25:46 -07:00
Saoud Rizwan 4ecccf5105 feat(cli): add mcp add shortcuts for stdio and http servers (#9773) 2026-03-11 16:17:43 -07:00
Ara 6d29bc6551 fix(prompt): resolve native tool placeholder interpolation (#9764)
* fix(prompt): resolve CWD and MULTI_ROOT_HINT in native tool schemas

* refactor(prompt): share multi-root hint constant
2026-03-11 08:10:58 -07:00
Saoud Rizwan 820057308a feat(cli): add kanban alias command (#9763) 2026-03-10 21:57:16 -07:00
Ara 7355f7e3b9 fix(gemini): cap flash output tokens to 8192 across providers (#9749) 2026-03-10 14:10:14 -07:00
Max 156f18f7b2 call telemetry service after initializeCli (#9752)
- calling telemetry service before initializeCli call causes a
"hostprovider not initialized error", which invokes errorservice, which
causes another "hostprovider not initialized error", which was breaking
this cline use case: echo "say hello" | cline

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-03-10 11:50:34 -07:00
Tomás Barreiro 6129caa423 Get user consent before loading images in Markdown (#9745)
* Add an UsafeImage handler that asks for consent before loading specific images

* Fix div as child of p

* Render self-contained images without consent

* Render alt conditionally and store approved src

* use a block span
2026-03-09 21:06:57 -07:00
alex-lum ffa4785f43 fix(telemetry): restore cache token and cost metrics in captureTokenUsage (#9741)
* feat(telemetry): restore cache token and cost metrics in captureTokenUsage

Add optional `options` parameter to `captureTokenUsage()` to record
cache write/read token counters and cost histograms that were
previously missing from telemetry.

- Extend `captureTokenUsage` with `cacheWriteTokens`, `cacheReadTokens`,
  and `totalCost` fields via an options object
- Record `cline.tokens.cache_write`, `cline.tokens.cache_read` counters/
  histograms and `cline.tokens.cost` histogram when provided
- Forward cache/cost data from both streaming `onUsageChunk` and
  `getApiStreamUsage` fallback call sites in the task loop
- Add 3 test cases covering options forwarding, undefined skipping,
  and event property inclusion

* refactor(telemetry): extract shared TokenUsage type and add value assertions

Address PR review feedback:
- Extract shared TokenUsage interface used by both captureTokenUsage and
  captureConversationTurnEvent, preventing future drift
- Add numeric value assertions for cache/cost counters and histograms
  so regressions recording wrong values are caught
2026-03-09 15:55:23 -07:00
CandiedUniverse 92ec126dca Fix Windows unit test path normalization (#9742) 2026-03-09 13:23:47 -07:00
Ara c4c8b16afb chore: remove changesets bot artifacts (#9739) 2026-03-09 12:26:48 -07:00
Max 2eed79dcd7 use setSessionOverride instead of setGlobalState (#9707)
- cli was storing fields to persistent state when it shouldn't be. The
value of these flags should only live for the duration of the CLI
session

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-03-09 11:20:57 -07:00
shey-cline d39a53feed Add Padding to Retry Message (#9635) 2026-03-09 09:29:27 -07:00
Dominic Cooney cae9bff416 test: rebaseline vertex_gemini3.tools.snap (#9731)
Commit 55569efb7 changed the list_code_definition_names path parameter
description but did not update the snapshot baseline.
2026-03-09 07:57:05 -07:00
Saoud Rizwan 36618fb327 fix(cli): remount TUI only on width resize (#9736) 2026-03-09 04:03:59 -07:00
Saoud Rizwan a335b5cb3e fix(cli): prevent startup prompt replay on resize remount (#9735) 2026-03-09 03:30:01 -07:00
Saoud Rizwan bce71b4448 feat(cli): add --continue for current directory (#9726)
* feat: add cli continue flag for current directory

* docs(cli): clarify continue command examples
2026-03-08 19:54:44 -07:00
Robin Newhouse f6d2b4d9ac Add --auto-condense CLI flag to enable AI-powered context compaction (#9705)
Exposes the existing useAutoCondense setting as a CLI flag, following
the same pattern as --double-check-completion. This allows enabling
auto-condense in eval runs (e.g. SWE-bench via Harbor) to reduce
context exhaustion failures.

Made-with: Cursor
2026-03-08 16:28:48 -07:00
Robin Newhouse 911fcfda47 gemini-hc: add parameter descriptions to Gemini tool schemas (#9681)
The Gemini converter was the only provider that didn't include
parameter-level descriptions in native tool call schemas. Anthropic
and OpenAI converters both resolve param.instruction into each
parameter's description field. This was missing for Google/Gemini,
meaning the model only saw parameter names and types with no
explanation of what each parameter expects.

Made-with: Cursor
2026-03-08 16:27:50 -07:00
Robin Newhouse 55569efb74 fix(tools): prevent crash when list_files/list_code_definition_names receives a file path (#9680)
listFiles() passed unvalidated paths as globby's `cwd`, crashing with
"The cwd option must be a path to a directory" when the model provided a
file path instead of a directory. This affected ~22% of SWE-bench tasks.

- Add isDirectory guard in listFiles() before calling globby
- Fix listFiles() to use resolved absolutePath for cwd instead of raw dirPath
- Return actionable error in parseSourceCodeForDefinitionsTopLevel when
  path is a file, guiding the model to use read_file instead
- Clarify list_code_definition_names parameter description to
  distinguish directory input from file input

Made-with: Cursor
2026-03-08 16:07:03 -07:00
Robin Newhouse 18561ed59d prompt: add test verification rules and make CLI_RULES language-agnostic (#9679)
- S1: Don't modify test assertions to match buggy code
- S2: Run project's existing test suite to verify fixes
- CLI_RULES: Remove Node.js-specific examples (npm/tsc)

Made-with: Cursor
2026-03-08 15:58:13 -07:00
Saoud Rizwan fa7265fa33 feat: add Anthropic Opus 4.6 fast mode variants (#9725)
* feat: add Anthropic Opus 4.6 fast mode variants

* refactor: localize Anthropic fast mode beta constant

* fix: correct Anthropic 1M fast mode pricing
2026-03-08 15:31:37 -07:00
M.Yoshida(Jyuko Co.,Ltd) bf96223639 docs: add .github/copilot-instructions.md for AI coding agents (#9606)
* docs: add .github/copilot-instructions.md for AI coding agents

* docs: update copilot-instructions with provider, state, and CLI guidance
2026-03-08 14:51:55 -07:00
Saoud Rizwan c489f79508 fix(cli): apply task flags before welcome TUI mount (#9721) 2026-03-07 11:20:53 -08:00
CandiedUniverse 6ccd662a1f Hooks: Add feature toggle (toggled off by default) (#9671)
* Add implementation plan doc

* feat(hooks): reintroduce runtime hooks feature toggle

* fix: thread effective hooks toggle through hook execution

* test: cover hooks feature toggle visibility and settings wiring

* Remove implementation plan doc

* Move Hooks toggle to Advanced section in Feature Settings

* Fixes as per PR feedback

* Clarifying hooksEnabled

* Make hooksEnabled true by default

* Further fixes as per Greptile feedback

* Further fixes as per Greptile feedback

* Fix failing tests
2026-03-06 13:49:03 -08:00
Tomás Barreiro a2fa4d3b92 [ENG-1571] Enable CLI Error autocapture (#9686)
* Introduce the enableErrorAutocapture option

* Enable capturing CLI extension

* Capture exception immediately

* Handled uncaptured exceptions

* refactor

* Add tests

* Capture unhandledExceptions

* Add an error boundary to the ink app

* Wrap the App in the ErrorBoundary

* Check for consent before capturing error

* Add context to the error capturing

* Fix tests

* refactor

* Remove `unref`
2026-03-06 20:39:44 +01:00
CandiedUniverse c8f81374a3 Fix flaky hooks tests on Windows (#9690)
* test(hooks): reduce Windows hook-runner launch overhead

test(hooks): make Windows hook tests deterministic

refactor(hooks): use platform-agnostic launcher cache naming

Improvements as per Cline code review feedback

test(hooks): remove direct-node acceleration path and stabilize windows timings

* test(hooks): add deterministic launcher cache concurrency coverage

* Improvements as per Cline code review feedback

* Update src/core/hooks/__tests__/hookprocess.test.ts

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

* Improvements as per Greptile feedback

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-06 10:46:34 -08:00
ClineXDiego eb25634bd4 fix(bedrock): handle thinking and redacted_thinking blocks in message conversion and streaming (#9424)
Fixes #9269 - Thinking blocks missing in Bedrock Opus 4.6

Changes:
- Add explicit handling for 'thinking' and 'redacted_thinking' content types
  in formatMessagesForConverseAPI() so they are silently skipped instead of
  triggering 'Unsupported content type: thinking' warnings
- Capture signature from additionalModelResponseFields thinking responses
- Add signature_delta handling in contentBlockDelta for streaming
- Add redacted_thinking block handling in contentBlockStart for streaming
- Extend ContentBlockStart/Delta interfaces with signature and data fields
- Add 'redacted_thinking' and 'document' to SupportedContentType union
- Add tests for thinking/redacted_thinking block filtering in message conversion
2026-03-06 10:41:43 -08:00
Max 1753d9dc15 release changelog and version bump (#9706)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-03-06 09:45:20 -08:00
Saoud Rizwan a562868cb9 feat(cli): add --auto-approve-all flag for interactive mode (#9698) 2026-03-06 02:08:59 -08:00
Saoud Rizwan bf5265758f feat(hooks): add Notification hook for attention boundaries (#9699)
* feat(hooks): add Notification hook for attention and completion

* chore(hooks): use default Notification template

* Revert "chore(hooks): use default Notification template"

This reverts commit 85f4e942fc.

* fix(hooks): escape JSON quotes in bash templates

* Revert "fix(hooks): escape JSON quotes in bash templates"

This reverts commit 29dfb9e01e.

* chore(hooks): preserve single-backslash template escaping

* fix(hooks): keep single-backslash JSON escaping in templates

* Revert "fix(hooks): keep single-backslash JSON escaping in templates"

This reverts commit b4c829489f.

* fix(hooks): keep escaped JSON echo template pattern
2026-03-06 02:07:25 -08:00
Saoud Rizwan 469752a201 feat(cli): add --hooks-dir flag for runtime hook injection (#9658)
* feat(cli): add --hooks-dir flag for runtime hook injection

Adds a --hooks-dir <path> CLI flag that allows passing an additional
hooks directory at spawn time. This enables orchestration tools (like
Kanbanana) to inject per-session lifecycle hooks without mutating
the user's global or workspace hooks directories.

The runtime hooks directory is included alongside existing global
(~/Documents/Cline/Hooks/) and workspace (.clinerules/hooks/)
directories during hook discovery. All hooks from all directories
are merged and run in parallel, so runtime hooks are purely additive.

* fix(cli): initialize runtime hooks before interactive startup
2026-03-05 23:33:41 -08:00
Ara 8fb2e8a3aa Adding gpt 5.4 models to chatgpt subscripiton (#9692)
* Adding gpt-5.4 to chatgpt subscription

* Fixing stuff

* Apply suggestion from @greptile-apps[bot]

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-03-05 18:06:17 -08:00
ClineXDiego aef32f52f9 fix: bypass git hooks on checkpoint initial commit (#9688)
Add --no-verify to the initial checkpoint commit in
CheckpointGitOperations.ts. This was already used for subsequent
commits in CheckpointTracker.ts but was missing from the initial
empty commit, causing Cline to fail to initialize when users have
global pre-commit hooks (e.g., conventional commits enforcement).

Fixes #9672
2026-03-05 19:02:00 -03:00
Dominic Cooney 2baf966db7 fix: handle streamableHttp reconnects and preserve OAuth redirect URIs across sessions (#9642)
- Make redirect URIs with dynamic ports valid, or reregister.
- Handle reconnects for streaming HTTP MCP servers.
2026-03-05 09:00:20 +09:00
alex-lum 152ba674da feat: add OTel tracking for AI output line/file metrics (lines added/deleted/changed, files created/deleted/moved) (#9562)
* feat: add telemetry for AI output accepted/rejected across tool handlers

Add line-level diff stats and file operation tracking to telemetry
events when users accept or reject tool outputs. Introduces a shared
`computeLineDiffStats` utility and `captureAiOutputAccepted`/
`captureAiOutputRejected` methods on the telemetry service, wired
into ApplyPatch, WriteToFile, ExecuteCommand, InsertContent, and
SearchAndReplace handlers.

* feat(telemetry): add source tracking for agent vs human edits

Add telemetry differentiation between agent-generated changes and
human modifications to capture more granular edit metrics:

- Add 'source' field to captureAiOutputAccepted telemetry events
- Track human edits by computing diff stats between agent's proposed
  content and final saved content
- Apply source tracking to ApplyPatchHandler and WriteToFileToolHandler
- Enable separate analytics for agent vs human contributions

This allows measuring how often and to what extent users modify
AI-generated code, providing insights into AI output quality and
user trust patterns.

* refactor(telemetry): centralize ai output attribution across file edit handlers

- add shared `AiOutputTelemetry` utility for accepted/rejected events
- refactor `WriteToFileToolHandler` and `ApplyPatchHandler` to use shared helpers
- preserve existing telemetry behavior (`source: "agent" | "human"`) while reducing duplication
- keep line diff/file-op attribution semantics unchanged

* fix(telemetry): use pre-save content for human edit line diff stats

The source:"human" telemetry was diffing agent content against
finalContent (post-save), which includes auto-formatting changes
from the editor. This inflated linesChanged/linesDeleted counts
when the formatter modified lines alongside the user's actual edits.

Use diff.applyPatch() to reconstruct the user's pre-save content
from the existing userEdits patch, excluding formatter noise from
the line diff stats.

* fixing syntax error

* refactor(telemetry): make next-hunk bounds check explicit

* remove comment

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

---------

Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
2026-03-04 14:45:57 -08:00
654 changed files with 44310 additions and 44301 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"cline": minor
---
Adds Messages API support to Oracle Code Assist, adding functionality for Claude models
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
Add /q command to quit CLI
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
Add Additional Markdown Formatting in CLI
@@ -1,16 +0,0 @@
---
"cline": patch
---
fix: resolve "Could not find the file context" error in Explain Changes comment replies
When clicking a line to start a discussion in the Explain Changes diff view, replies would
intermittently fail with "Error: Could not find the file context". This happened because
the reply handler and the `onCommentStart` callback were using a strict `absolutePath`-only
match to look up files in `changedFiles`, while the VS Code comment controller may return
paths in different formats (relative vs. absolute, different separators on Windows, etc.).
Fixed by adding a `relativePath` fallback in both lookup sites, making them consistent with
the already-correct logic in `streamAIExplanationComments`.
Fixes #9382
-18
View File
@@ -1,18 +0,0 @@
---
"cline": patch
---
fix: clear all OCA secrets on auth refresh failure to prevent re-auth loop
When OCA (Oracle Code Assist) token refresh fails with 400 invalid_grant or 401,
the stale secrets were not fully cleared from storage. The `clearAuth()` method
only cleared `ocaApiKey` and `ocaRefreshToken`, leaving legacy secrets
`ocaAccessToken` and `ocaTokenSet` (set by older Cline versions) in VS Code's
secret storage. These stale secrets caused every subsequent re-auth attempt to
fail in a loop, requiring manual SQLite deletion to recover.
Fix:
- Added `ocaAccessToken` and `ocaTokenSet` to `SecretKeys` in `state-keys.ts`
- Updated `OcaAuthProvider.clearAuth()` to clear all 4 OCA secrets
Fixes #9567
@@ -1,9 +0,0 @@
---
"claude-dev": patch
---
Fix OpenAI-compatible `gpt-oss` native tool mode so file editing works reliably:
- Enable `apply_patch` for `gpt-oss` models when using native GPT-5 prompt variants.
- Add regression tests covering model family selection and tool availability.
- Add a smoke-test scenario for OpenAI-compatible `gpt-oss` file editing and improve the smoke runner for per-scenario auth/env requirements.
-5
View File
@@ -1,5 +0,0 @@
---
"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
---
Use JSON_SCHEMA for yaml.load to prevent unsafe deserialization from untrusted sources
-4
View File
@@ -1,4 +0,0 @@
"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
@@ -1,5 +0,0 @@
---
"cline": patch
---
Add automatic retries (up to 3 attempts) for smoke test CI jobs to reduce flaky failures
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
add focus ring on action buttons
-5
View File
@@ -1,5 +0,0 @@
---
"cline": patch
---
fix acp auth check so acp mode can be used with more providers
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Update SambaNova Provider models list and add temperature for models
-33
View File
@@ -1,33 +0,0 @@
# CLI Development
The CLI lives in `cli/` and uses React Ink for terminal UI.
- If needed, look at `cli/src/constants/colors.ts` for re-used terminal colors, e.g. `COLORS.primaryBlue` highlight color (selections, spinners, success states).
- Never use `dimColor` with gray (e.g. `<Text color="gray" dimColor>`) - it's too hard to read. Use `color="gray"` for secondary text and normal foreground (no color) for primary text.
- When thinking about how to handle state or messages from core, look at webview for how it communicates with the vs code extension.
- When updating the webview, consider and suggest to the user to update the CLI TUI since we want to provide a similar experience to our terminal users as we do our vs code extension users.
## Adding New API Providers
When adding a new API provider to the extension, you must also update the CLI:
1. **Update `cli/src/components/ModelPicker.tsx`**: Add the provider to the `providerModels` map so `getDefaultModelId()` returns the correct default model. Import the models and default ID from `@shared/api`:
```typescript
import { newProviderDefaultModelId, newProviderModels } from "@/shared/api"
export const providerModels = {
// ...existing providers
"new-provider": { models: newProviderModels, defaultId: newProviderDefaultModelId },
}
```
2. **Use `applyProviderConfig()` for auth flows**: When implementing OAuth or other auth flows for the provider, use the shared utility at `cli/src/utils/provider-config.ts`:
```typescript
import { applyProviderConfig } from "../utils/provider-config"
// After successful auth:
await applyProviderConfig({ providerId: "new-provider", controller })
```
This handles setting provider, default model, API key mapping, state persistence, and rebuilding the API handler.
3. **Provider-specific auth**: If the provider uses OAuth (like `openai-codex`), add handling in `SettingsPanelContent.tsx`'s `handleProviderSelect` callback. See the existing Codex OAuth flow as a reference.
+121
View File
@@ -0,0 +1,121 @@
# Debug Harness
HTTP-controlled debugger for the VSCode extension at `src/dev/debug-harness/server.ts`.
## Quick start
```bash
# Build extension first if needed (protos + esbuild):
npm run protos && IS_DEV=true node esbuild.mjs
# Launch (skip-build if already built):
npx tsx src/dev/debug-harness/server.ts --skip-build --auto-launch
# In another terminal:
curl localhost:19229/api -d '{"method":"status"}'
```
## Data Isolation
The debugee runs with `CLINE_DIR=~/.cline2` by default, separate from your real `~/.cline`.
This prevents the debugee's logout from logging out the debugger, and vice versa.
Override with `--cline-dir /tmp/test-dir`. Check with `status()``clineDir`.
## Browser Capture & OAuth
The debugee runs with `CLINE_CAPTURE_BROWSER=1`, which intercepts `openExternal()` in
`src/utils/env.ts`. URLs are captured instead of opening a real browser:
- Logged to `$CLINE_DIR/data/debug-captured-urls.jsonl`
- POSTed in real-time to `/captured-url` on the harness server
- Queryable via `oauth.captured_urls`
### OAuth API
- **`oauth.captured_urls`** `{clear?}` — URLs the debugee tried to open
- **`oauth.read_stored_token`** — Check auth token presence in secrets.json
- **`oauth.simulate_callback`** `{path, code?, state?, provider?, token?}` — Build vscode:// callback URI
- **`oauth.read_captured_urls_file`** — Read on-disk JSONL of captured URLs
### OAuth testing flow
For **Cline OAuth** (SDK local callback): The SDK starts a local HTTP server, the auth URL
is captured. To complete: open the captured URL in a real browser (it redirects back to the
SDK's callback server), OR extract the callback port and `curl http://127.0.0.1:PORT/callback?code=...`.
For **MCP/Provider OAuth** (vscode:// URI): The redirect goes to a vscode:// URI. Use
`oauth.simulate_callback` to build it, then inject via `ext.evaluate` calling the URI handler.
## Navigating Views — Use Commands, Not Clicks
Don't try to find/click small sidebar icons. Use VSCode commands via command palette.
Registered in `src/registry.ts`:
| Command | View |
|---------|------|
| `cline.accountButtonClicked` | Account / sign-in |
| `cline.historyButtonClicked` | Task history |
| `cline.settingsButtonClicked` | Settings |
| `cline.mcpButtonClicked` | MCP servers |
| `cline.plusButtonClicked` | New task (chat) |
| `cline.worktreesButtonClicked` | Worktrees |
```bash
curl localhost:19229/api -d '{"method":"ui.command_palette","params":{"command":"cline.accountButtonClicked"}}'
```
## Key commands
All via `POST localhost:19229/api` with `{"method":"...", "params":{...}}`:
- **`launch`** / **`shutdown`** — lifecycle
- **`ui.screenshot`** — screenshot to `/tmp/cline-debug/`; returns `{path}`**use `read_file` on the path to examine, do NOT `open` the file** (Preview.app covers the VSCode window)
- **`ui.open_sidebar`** — open the Cline sidebar
- **`ext.set_breakpoint`** `{file, line, condition?}` — breakpoint by source file (sourcemap-resolved)
- **`ext.evaluate`** `{expression, callFrameId?}` — eval in extension host
- **`ext.resume`** / **`ext.step_over`** / **`ext.step_into`** — stepping
- **`ext.call_stack`** — inspect when paused
- **`web.evaluate`** `{expression}` — eval in webview
- **`web.post_message`** `{message}` — send postMessage to extension host via exposed vsCodeApi
- **`wait_for_pause`** `{timeout?}` — block until breakpoint hit
- **`ui.locator`** `{role?, testId?, text?, frame?}` — Playwright locator (auto-retries on stale sidebar frame)
- **`ui.react_input`** `{text, selector?, clear?, submit?}` — set React textarea value via `execCommand('insertText')`; works reliably across multiple tasks
- **`ui.send_message`** `{text, images?, files?, responseType?}` — send chat message bypassing the textarea entirely (via gRPC postMessage)
- **`ui.command_palette`** `{command}` — run VSCode command
## Typical Session
```bash
# 1. Launch
curl localhost:19229/api -d '{"method":"launch","params":{"skipBuild":true}}'
# 2. Open sidebar + dismiss overlays (ALWAYS do this first)
curl localhost:19229/api -d '{"method":"ui.open_sidebar"}'
curl localhost:19229/api -d '{"method":"web.evaluate","params":{"expression":"document.querySelectorAll(\".sr-only\").forEach(el => el.parentElement?.click())"}}'
# 3. Navigate to view
curl localhost:19229/api -d '{"method":"ui.command_palette","params":{"command":"cline.accountButtonClicked"}}'
# 4. Check captured OAuth URLs if testing auth
curl localhost:19229/api -d '{"method":"oauth.captured_urls"}'
# 5. Verify
curl localhost:19229/api -d '{"method":"ui.screenshot"}'
```
## Caveats
- **⚠️ Dismiss promotional overlays FIRST**: On fresh launches, full-screen promo overlays block the sidebar. **Dismiss immediately after `ui.open_sidebar`**, before any other interaction or screenshot. May need to run twice:
```bash
curl localhost:19229/api -d '{"method": "ui.open_sidebar"}'
curl localhost:19229/api -d '{"method": "web.evaluate", "params": {"expression": "document.querySelectorAll(\".sr-only\").forEach(el => el.parentElement?.click())"}}'
```
- **Screenshots — don't open the file**: `ui.screenshot` and `ui.sidebar_screenshot` save PNGs to `/tmp/cline-debug/` and return the `{path}`. Use `read_file` on that path to examine screenshots. Running `open <path>` launches Preview.app on macOS which covers the VSCode window.
- **Scripts count = 0 after launch**: CDP connects after extension host starts, so scripts parsed during startup aren't tracked. Breakpoints still work via sourcemap resolution.
- **Port 9230**: Extension host inspector. If another VSCode instance uses this port, the harness will fail to connect. Kill other debug instances first.
- **macOS only** for now (Playwright Electron launch behavior).
- **Webview CDP**: `connect_webview` may fail depending on Electron version. `web.evaluate` still works via Playwright's `frame.evaluate()` fallback.
- **Sourcemap paths**: esbuild outputs relative paths like `../src/extension.ts` in the sourcemap. The resolver handles this, but if a file isn't found, use `ext.source_files` to see exact paths.
- **OAuth with fake codes**: Browser capture intercepts the URL but doesn't provide a valid auth code. For real OAuth testing, open the captured URL in a browser. For unit testing, mock the token exchange.
See `src/dev/debug-harness/README.md` for full API reference.
+43 -87
View File
@@ -18,6 +18,49 @@ This file is the secret sauce for working effectively in this codebase. It captu
- 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
## Searching the Codebase — Avoiding Build Output
Several directories contain build output or generated code that produces
noisy or unusable results with `search_files` / `grep`:
| Directory | What it is | Why it's a problem |
|-----------|-----------|-------------------|
| `out/` | esbuild bundle output | Mirrors `src/` structure as minified JS — every search gets duplicate hits on single-line files |
| `dist/` | Packaged extension | Entire extension bundled into one minified `extension.js` (~1 long line) |
| `dist-standalone/` | Standalone build output | Same minification issue |
| `src/generated/` | Generated protobuf code | Auto-generated from `proto/`; not the source of truth |
| `src/shared/proto/` | Generated proto type defs | Auto-generated from `proto/`; not the source of truth |
| `node_modules/` | Dependencies | Huge, not project source |
### How to skip build output
**`search_files`** — Point at `src/` (not the project root) and use `file_pattern`:
```
search_files(path="src/core", regex="myFunction", file_pattern="*.ts")
```
The `file_pattern` parameter is the most effective filter — e.g. `"*.ts"`,
`"*.tsx"`, `"*.proto"`.
**`grep` directly** — Exclude build dirs and restrict to source extensions:
```bash
grep -rn "myFunction" src/ --include="*.ts" --exclude-dir={out,dist,node_modules,generated}
```
### When you must search minified files
Sometimes you need to verify what got bundled (e.g., checking if a change
made it into the build). Minified files are typically one long line, so
normal `grep` shows the entire file as context. Use these approaches:
- **`grep -oP`** to extract just the match with limited surrounding context:
```bash
grep -oP '.{0,40}myFunction.{0,40}' dist/extension.js
```
- **`read_file`** on files in `out/src/` — these have source maps and are
more readable than `dist/extension.js` (which is the fully bundled output).
- **Source maps** — `out/src/*.js.map` and `dist/extension.js.map` can be
used to trace minified output back to original source locations.
## gRPC/Protobuf Communication
The extension and webview communicate via gRPC-like protocol over VS Code message passing.
@@ -48,93 +91,6 @@ The extension and webview communicate via gRPC-like protocol over VS Code messag
- `src/core/controller/task/explainChanges.ts` - Handler implementation
- `webview-ui/src/components/chat/ChatRow.tsx` - UI rendering
## Adding a New API Provider
When adding a new provider (e.g., "openai-codex"), you must update the proto conversion layer in THREE places or the provider will silently reset to Anthropic:
1. `proto/cline/models.proto` - Add to the `ApiProvider` enum (e.g., `OPENAI_CODEX = 40;`)
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts` - Add case mapping string to proto enum
3. `convertProtoToApiProvider()` in the same file - Add case mapping proto enum back to string
**Why this matters:** Without these, the provider string hits the `default` case and returns `ANTHROPIC`. The webview, provider list, and handler all work fine, but the state silently resets when it round-trips through proto serialization. No error is thrown.
**Other files to update when adding a provider:**
- `src/shared/api.ts` - Add to `ApiProvider` union type, define models
- `src/shared/providers/providers.json` - Add to provider list for dropdown
- `src/core/api/index.ts` - Register handler in `createHandlerForProvider()`
- `webview-ui/src/components/settings/utils/providerUtils.ts` - Add cases in `getModelsForProvider()` and `normalizeApiConfiguration()`
- `webview-ui/src/utils/validate.ts` - Add validation case
- `webview-ui/src/components/settings/ApiOptions.tsx` - Render provider component
## Responses API Providers (OpenAI Codex, OpenAI Native)
Providers using OpenAI's Responses API require native tool calling. XML tools don't work with the Responses API.
**Symptoms of broken native tool calling:**
- Tools get called multiple times (e.g., `ask_followup_question` asks the same question twice)
- Tool arguments get duplicated or malformed
- The model responds but tools aren't recognized
**Root causes to check:**
1. **Provider missing from `isNextGenModelProvider()`** in `src/utils/model-utils.ts`. The native variant matchers (e.g., `native-gpt-5/config.ts`) call this function. If your provider isn't in the list, the matcher returns false and falls back to XML tools.
2. **Model missing `apiFormat: ApiFormat.OPENAI_RESPONSES`** in its model info (`src/shared/api.ts`). This property signals that the model requires native tool calling. The task runner in `src/core/task/index.ts` checks this and forces `enableNativeToolCalls: true` regardless of user settings.
**When adding a new Responses API provider:**
1. Add provider to `isNextGenModelProvider()` list in `src/utils/model-utils.ts`
2. Set `apiFormat: ApiFormat.OPENAI_RESPONSES` on all models that use the Responses API
3. The variant matcher and task runner will handle the rest automatically
## Adding Tools to System Prompt
This is tricky—multiple prompt variants and configs. **Always search for existing similar tools first and follow their pattern.** Look at the full chain from prompt definition → variant configs → handler → UI before implementing.
1. **Add to `ClineDefaultTool` enum** in `src/shared/tools.ts`
2. **Tool definition** in `src/core/prompts/system-prompt/tools/` (create file like `generate_explanation.ts`)
- Define variants for each `ModelFamily` (generic, next-gen, xs, etc.)
- Export variants array (e.g., `export const my_tool_variants = [GENERIC, NATIVE_NEXT_GEN, XS]`)
- **Fallback behavior**: If a variant isn't defined for a model family, `ClineToolSet.getToolByNameWithFallback()` automatically falls back to GENERIC. So you only need to export `[GENERIC]` unless the tool needs model-specific behavior.
3. **Register in `src/core/prompts/system-prompt/tools/init.ts`** - Import and spread into `allToolVariants`
4. **Add to variant configs** - Each model family has its own config in `src/core/prompts/system-prompt/variants/*/config.ts`. Add your tool's enum to the `.tools()` list:
- `generic/config.ts`, `next-gen/config.ts`, `gpt-5/config.ts`, `native-gpt-5/config.ts`, `native-gpt-5-1/config.ts`, `native-next-gen/config.ts`, `gemini-3/config.ts`, `glm/config.ts`, `hermes/config.ts`, `xs/config.ts`
- **Important**: If you add to a variant's config, make sure the tool spec exports a variant for that ModelFamily (or relies on GENERIC fallback)
5. **Create handler** in `src/core/task/tools/handlers/`
6. **Wire up in `ToolExecutor.ts`** if needed for execution flow
7. **Add to tool parsing** in `src/core/assistant-message/index.ts` if needed
8. **If tool has UI feedback**: add `ClineSay` enum in proto, update `src/shared/ExtensionMessage.ts`, update `src/shared/proto-conversions/cline-message.ts`, update `webview-ui/src/components/chat/ChatRow.tsx`
## Modifying System Prompt
**Read these first:** `src/core/prompts/system-prompt/README.md`, `tools/README.md`, `__tests__/README.md`
System prompt is modular: **components** (reusable sections) + **variants** (model-specific configs) + **templates** (with `{{PLACEHOLDER}}` resolution).
**Key directories:**
- `components/` - Shared sections: `rules.ts`, `capabilities.ts`, `editing_files.ts`, etc.
- `variants/` - Model-specific: `generic/`, `next-gen/`, `xs/`, `gpt-5/`, `gemini-3/`, `hermes/`, `glm/`, etc.
- `templates/` - Template engine and placeholder definitions
**Variant tiers (ask user which to modify):**
- **Next-gen** (Claude 4, GPT-5, Gemini 2.5): `next-gen/`, `native-next-gen/`, `native-gpt-5/`, `native-gpt-5-1/`, `gemini-3/`, `gpt-5/`
- **Standard** (default fallback): `generic/`
- **Local/small models**: `xs/`, `hermes/`, `glm/`
**How overrides work:** Variants can override components via `componentOverrides` in their `config.ts`, or provide a custom template in `template.ts` (e.g., `next-gen/template.ts` exports `rules_template`). If no override, the shared component from `components/` is used.
**Example: Adding a rule to RULES section**
1. Check if variant overrides rules: look for `rules_template` in `variants/*/template.ts` or `componentOverrides.RULES` in `config.ts`
2. If shared: modify `components/rules.ts`
3. If overridden: modify that variant's template
4. XS variant is special—has heavily condensed inline content in `template.ts`
**After any changes, regenerate snapshots:**
```bash
UPDATE_SNAPSHOTS=true npm run test:unit
```
Snapshots live in `__tests__/__snapshots__/`. Tests validate across model families and context variations (browser, MCP, focus chain).
## Modifying Default Slash Commands
Three places need updates:
- `src/core/slash-commands/index.ts` - Command definitions
- `src/core/prompts/commands.ts` - System prompt integration
- `webview-ui/src/utils/slash-commands.ts` - Webview autocomplete
## Adding New Global State Keys
Adding a new key to global state requires updates in multiple places. Missing any step causes silent failures.
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "PostToolUse running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "PostToolUse response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "PostToolUse hook custom errorMessage"
}
EOF
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "PreToolUse running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "PreToolUse response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "PreToolUse hook custom errorMessage"
}
EOF
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "TaskCancel running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskCancel response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskCancel hook custom errorMessage"
}
EOF
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "TaskResume running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskResume response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskResume hook custom errorMessage"
}
EOF
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "TaskStart running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "TaskStart response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "TaskStart hook custom errorMessage"
}
EOF
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "UserPromptSubmit running inside local cline/.clinerules/hooks/ directory"
input=$(cat)
echo $input | jq .
for i in {1..5}; do
sleep 1
echo "$i"
done
cat <<EOF
{
"cancel": false,
"contextModification": "UserPromptSubmit response from the local cline/.clinerules/hooks/ directory.",
"errorMessage": "UserPromptSubmit hook custom errorMessage"
}
EOF
-90
View File
@@ -1,90 +0,0 @@
# Networking & Proxy Support
To ensure Cline works correctly in all environments (VSCode, JetBrains, CLI) and with various network configurations (especially corporate proxies), strictly follow these guidelines for all network activity.
In extension code, do NOT use the global `fetch` or a default `axios` instance. (Note, `shared/net.ts` is exempt from these rules because it sets up the fetch wrappers.) In Webview code, you SHOULD use global `fetch`.
Global `fetch` and default `axios` do not automatically pick up proxy configurations in all environments (specifically JetBrains and CLI). You MUST use the provided utilities in `@/shared/net` which handle proxy agent configuration. In the webview, the browser/embedder handles proxies.
## Guidelines
### 1. Using `fetch`
Instead of `fetch(...)`, import the proxy-aware wrapper:
```typescript
import { fetch } from '@/shared/net'
// Usage is identical to global fetch
const response = await fetch('https://api.example.com/data')
```
### 2. Using `axios`
When using `axios`, you must apply the settings from `getAxiosSettings()`:
```typescript
import axios from 'axios'
import { getAxiosSettings } from '@/shared/net'
const response = await axios.get('https://api.example.com/data', {
headers: { 'Authorization': '...' },
...getAxiosSettings() // <--- CRITICAL: Injects the proxy agent if needed
})
```
### 3. Third-Party Clients (OpenAI, Ollama, etc.)
Most API client libraries allow you to customize the `fetch` implementation. You **MUST** pass the proxy-aware `fetch` to these clients.
**Example (OpenAI):**
```typescript
import OpenAI from "openai"
import { fetch } from "@/shared/net"
this.client = new OpenAI({
apiKey: '...',
fetch, // <--- CRITICAL: Pass our fetch wrapper
})
```
### 4. Tests
Use `mockFetchForTesting` to mock the underlying fetch implementation.
**Example (callback):**
```
import { mockFetchForTesting } from "@/shared/net"
...
let mockFetch = ...
mockFetchForTesting(mockFetch, () => {
// This calls mockFetch
fetch('https://foo.example').then(...)
})
// Original fetch is restored immediately when the call returns.
```
**Example (Promise):**
```
import { mockFetchForTesting } from "@/shared/net"
...
let mockFetch = ...
await mockFetchForTesting(mockFetch, async () => {
await ...
// This calls mockFetch
await fetch('https://foo.example')
...
})
// Original fetch is restored when the Promise from the callback settles
```
## Verification
If you are adding a new network call or integration:
1. Check `@/shared/net.ts` is imported.
2. Ensure `fetch` or `getAxiosSettings` is being used.
3. Verify that third-party clients are configured to use the custom fetch.
+29
View File
@@ -0,0 +1,29 @@
# SDK Migration
When working on the SDK migration (branch `sdk-migration-v3`), start by
reading `sdk-migration/README.md` in full. It contains the step-by-step
plan, core principles, and operational procedure.
Key documents:
- `sdk-migration/README.md` — Entry point, plan, steps
- `sdk-migration/ARCHITECTURE.md` — Design decisions, features, SDK capabilities
- `sdk-migration/SDK-REFERENCE/OAUTH.md` — SDK OAuth reference
- `sdk-migration/SDK-REFERENCE/MCP.md` — SDK MCP reference
- `sdk-migration/PROBLEMS.md` — Issue tracker with verification status
- `src/dev/debug-harness/README.md` — Debug harness API
## Critical Rules
1. **Always use `kb_search(name="sdk", query="...")` before implementing**
SDK features. Don't guess at APIs.
2. **Never mark a problem 🟢 without evidence.** Write the test first.
3. **Delete and document.** When replacing a classic module, delete it
immediately and add `// Replaces classic src/core/... (see origin/main)`.
Use `kb_search(name="cline", commit="origin/main")` or
`git show origin/main:path` to reference the classic implementation.
4. **Single entry point.** No `CLINE_SDK` env variable. There is one
codepath — the SDK adapter.
5. **Use `{appBaseUrl}`**, never hardcode `app.cline.bot`.
6. **Avoid `as` casts.** Use explicit conversion functions with tests.
7. **Dismiss the Kanban overlay** before any debug harness interaction.
8. **Use command palette** to navigate tabs in the debug harness.
+2
View File
@@ -1,4 +1,6 @@
demo.gif filter=lfs diff=lfs merge=lfs -text
assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
webview-ui/src/assets/cline_kanban_demo.mp4 filter=lfs diff=lfs merge=lfs -text
webview-ui/src/assets/cline_kanban_demo.webm filter=lfs diff=lfs merge=lfs -text
* text=auto eol=lf
+6
View File
@@ -26,6 +26,12 @@ body:
placeholder: 'e.g., 1.2.3'
validations:
required: true
- type: checkboxes
id: beta
attributes:
label: Beta version
options:
- label: I am using a beta version of Cline
- type: textarea
id: what-happened
attributes:
+58
View File
@@ -0,0 +1,58 @@
# Copilot Instructions for Cline
This is a VS Code extension. Read `.clinerules/general.md` for tribal knowledge and nuanced patterns.
## Architecture
- **Core** (`src/`): `extension.ts``WebviewProvider``Controller` (single source of truth) → `Task` (agent loop).
- **Webview** (`webview-ui/`): React/Vite app. State via `ExtensionStateContext.tsx`, synced through message passing.
- **CLI** (`cli/`): React Ink terminal UI sharing core logic. Update CLI when changing webview features.
- **Communication**: Protobuf-defined gRPC-like protocol over VS Code message passing. Schemas in `proto/`.
- **MCP**: `src/services/mcp/McpHub.ts`.
## Build & Test (Critical — non-obvious commands)
- **Build**: `npm run compile` — NOT `npm run build`.
- **Watch**: `npm run watch` (extension + webview).
- **Protos**: `npm run protos` — run **immediately** after any `.proto` change. Generates into `src/shared/proto/`, `src/generated/`.
- **Tests**: `npm run test:unit`. After prompt/tool changes: `UPDATE_SNAPSHOTS=true npm run test:unit`.
## Protobuf RPC Workflow (4 steps)
1. **Define** in `proto/cline/*.proto`. Naming: `PascalCaseService`, `camelCase` RPCs, `PascalCase` Messages. Use `common.proto` shared types for simple data.
2. **Generate**: `npm run protos`.
3. **Backend handler**: `src/core/controller/<domain>/`.
4. **Frontend call**: `UiServiceClient.myMethod(Request.create({...}))`.
- Adding enums (e.g. `ClineSay`) → also update `src/shared/proto-conversions/cline-message.ts`.
## Adding API Providers (silent failure risk)
Three proto conversion updates are **required** or the provider silently resets to Anthropic:
1. `proto/cline/models.proto` — add to `ApiProvider` enum.
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts`.
3. `convertProtoToApiProvider()` in the same file.
Also update: `src/shared/api.ts`, `src/shared/providers/providers.json`, `src/core/api/index.ts`, `webview-ui/.../providerUtils.ts`, `webview-ui/.../validate.ts`, `webview-ui/.../ApiOptions.tsx`, and `cli/src/components/ModelPicker.tsx`.
For Responses API providers: add to `isNextGenModelProvider()` in `src/utils/model-utils.ts` and set `apiFormat: ApiFormat.OPENAI_RESPONSES` on models.
## Adding Tools to System Prompt (5+ file chain)
1. Add enum to `ClineDefaultTool` in `src/shared/tools.ts`.
2. Create definition in `src/core/prompts/system-prompt/tools/` (export `[GENERIC]` minimum).
3. Register in `src/core/prompts/system-prompt/tools/init.ts`.
4. Whitelist in `src/core/prompts/system-prompt/variants/*/config.ts` for each model family.
5. Handler in `src/core/task/tools/handlers/`, wire in `ToolExecutor.ts`.
6. If tool has UI: add `ClineSay` enum in proto → `ExtensionMessage.ts``cline-message.ts``ChatRow.tsx`.
7. Regenerate snapshots: `UPDATE_SNAPSHOTS=true npm run test:unit`.
## Modifying System Prompt
Modular: `components/` (shared) + `variants/` (model-specific) + `templates/` (`{{PLACEHOLDER}}`). Variants override components via `componentOverrides` in `config.ts` or custom `template.ts`. XS variant is heavily condensed inline. Always regenerate snapshots after changes.
## Global State Keys (silent failure risk)
Adding a key requires: type in `src/shared/storage/state-keys.ts`, read via `context.globalState.get()` in `src/core/storage/utils/state-helpers.ts` `readGlobalStateFromDisk()`, and add to return object. Missing the `.get()` call compiles fine but value is always `undefined`.
## Slash Commands (3 places)
- `src/core/slash-commands/index.ts` — definitions.
- `src/core/prompts/commands.ts` — system prompt integration.
- `webview-ui/src/utils/slash-commands.ts` — webview autocomplete.
## Conventions
- **Paths**: Always use `src/utils/path` helpers (`toPosixString`) for cross-platform compatibility.
- **Logging**: `src/shared/services/Logger.ts`.
- **Feature flags**: See PR #7566 as reference pattern.
+83
View File
@@ -0,0 +1,83 @@
name: CLI TUI Tests
on:
pull_request:
branches:
- main
workflow_dispatch:
workflow_call:
permissions:
contents: read
jobs:
cli-tui-tests:
name: CLI TUI Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build CLI
run: npm run cli:build
- name: Run TUI Tests
id: tui_tests
run: |
npm run test:e2e:cli:tui 2>&1 | tee tui-test-output.log
exit_code=${PIPESTATUS[0]}
echo "tui_exit_code=$exit_code" >> $GITHUB_OUTPUT
exit $exit_code
- name: Write failure summary
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
run: |
echo "## ❌ CLI TUI Tests Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Step outcome:** \`${{ steps.tui_tests.outcome }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Test Output" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
if [ -f tui-test-output.log ]; then
cat tui-test-output.log >> $GITHUB_STEP_SUMMARY
else
echo "(no test output captured — process may have been killed before output was flushed)" >> $GITHUB_STEP_SUMMARY
fi
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Debugging" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **TUI traces** are attached as artifacts below — download and inspect them to see terminal state at the point of failure." >> $GITHUB_STEP_SUMMARY
echo "- **To view a trace replay/Run a TUI Trace: ** run \`npx tui-test show-trace path/to/trace/file\` in your terminal" >> $GITHUB_STEP_SUMMARY
echo "- **Full test log** is also attached as an artifact." >> $GITHUB_STEP_SUMMARY
echo "- Tests run with \`retries: 2\` so any failure shown is a consistent failure, not a flake." >> $GITHUB_STEP_SUMMARY
- name: Upload TUI traces
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
uses: actions/upload-artifact@v4
with:
name: tui-test-traces
path: tests/e2e/cli/tui-traces/
retention-days: 14
if-no-files-found: warn
- name: Upload test log
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
uses: actions/upload-artifact@v4
with:
name: tui-test-log
path: tui-test-output.log
retention-days: 14
if-no-files-found: warn
@@ -51,3 +51,15 @@ jobs:
});
}
}
// Check if beta version checkbox is checked
if (body.includes('- [X] I am using a beta version of Cline') || body.includes('- [x] I am using a beta version of Cline')) {
if (!labels.includes('beta')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['beta']
});
}
}
+18
View File
@@ -91,3 +91,21 @@ jobs:
echo ""
echo "📦 Install with: npm install -g cline"
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
- name: Post release to Slack
uses: slackapi/slack-github-action@v3.0.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
payload: |
channel: "C0APVKGGZFC"
text: "Cline CLI v${{ steps.version.outputs.version }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Cline CLI v${{ steps.version.outputs.version }}*"
- type: "context"
elements:
- type: "mrkdwn"
text: "<https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}|View on npm>"
+3 -1
View File
@@ -31,8 +31,10 @@ jobs:
- name: Check for recent commits
id: check_commits
env:
FORCE_PUBLISH: ${{ inputs.force_publish }}
run: |
if [ "${{ inputs.force_publish }}" = "true" ]; then
if [ "$FORCE_PUBLISH" = "true" ]; then
echo "force_publish enabled, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
@@ -30,7 +30,11 @@ permissions:
pull-requests: write # Required by nested reusable test workflow
jobs:
cli-tui-tests:
uses: ./.github/workflows/cli-tui-tests.yml
publish-main:
needs: cli-tui-tests
if: |
github.repository == 'cline/cline' && (
github.event_name == 'workflow_dispatch' &&
@@ -44,6 +48,7 @@ jobs:
confirm_publish: ${{ github.event.inputs.confirm_publish }}
publish-nightly:
needs: cli-tui-tests
if: |
github.repository == 'cline/cline' && (
github.event_name == 'schedule' ||
+72
View File
@@ -0,0 +1,72 @@
name: "Publish SDK Nightly Release"
on:
schedule:
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
permissions:
contents: read
packages: write
checks: write
pull-requests: write
env:
# Keep the publish source pinned to one reviewed branch instead of accepting arbitrary refs.
SDK_NIGHTLY_REF: dpc/sdk-migration-simpler-login
jobs:
publish:
name: Publish Cline (Nightly SDK) Extension
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: PublishNightly
steps:
- name: Checkout trusted SDK nightly branch
uses: actions/checkout@v4
with:
ref: ${{ env.SDK_NIGHTLY_REF }}
lfs: true
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
# 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
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
- name: Verify LFS media assets are resolved
run: |
for FILE in webview-ui/src/assets/cline_kanban_demo.mp4 webview-ui/src/assets/cline_kanban_demo.webm; do
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
exit 1
fi
done
- name: Publish SDK nightly extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
run: npm run publish:marketplace:nightly
+12 -3
View File
@@ -1,8 +1,6 @@
name: "Publish Nightly Release"
on:
schedule:
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
permissions:
@@ -24,6 +22,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
lfs: true
- name: Check for recent commits
run: |
@@ -49,7 +49,16 @@ jobs:
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
- name: Publish Extension as Pre-release
- name: Verify LFS media assets are resolved
run: |
for FILE in webview-ui/src/assets/cline_kanban_demo.mp4 webview-ui/src/assets/cline_kanban_demo.webm; do
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
exit 1
fi
done
- name: Publish Nightly Extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
+37 -3
View File
@@ -43,12 +43,14 @@ jobs:
ref: main
fetch-depth: 0
fetch-tags: true
lfs: true
- name: Resolve Release Tag
id: resolve_tag
env:
TAG: ${{ github.event.inputs.tag }}
AUTO_CREATE: ${{ github.event.inputs.auto_create_tag_from_main }}
run: |
TAG="${{ github.event.inputs.tag }}"
AUTO_CREATE="${{ github.event.inputs.auto_create_tag_from_main }}"
TESTED_SHA="${{ github.sha }}"
WORKFLOW_REF="${{ github.ref }}"
@@ -133,6 +135,15 @@ jobs:
fi
echo "Tag and package version match: $TAG"
- name: Verify LFS media assets are resolved
run: |
for FILE in webview-ui/src/assets/cline_kanban_demo.mp4 webview-ui/src/assets/cline_kanban_demo.webm; do
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
exit 1
fi
done
- name: Package and Publish Extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
@@ -147,11 +158,12 @@ jobs:
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
RELEASE_TYPE: ${{ github.event.inputs.release-type }}
run: |
# Required to generate the .vsix
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
if [ "${{ github.event.inputs.release-type }}" = "pre-release" ]; then
if [ "$RELEASE_TYPE" = "pre-release" ]; then
npm run publish:marketplace:prerelease
echo "Successfully published pre-release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
else
@@ -187,3 +199,25 @@ jobs:
prerelease: ${{ github.event.inputs.release-type == 'pre-release' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Post release to Slack
uses: slackapi/slack-github-action@v3.0.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
payload: |
channel: "C0APVKGGZFC"
text: "Cline ${{ steps.resolve_tag.outputs.tag }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Cline ${{ steps.resolve_tag.outputs.tag }}*"
- type: "section"
text:
type: "mrkdwn"
text: ${{ toJSON(steps.changelog.outputs.content) }}
- type: "context"
elements:
- type: "mrkdwn"
text: "Full Changelog: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.resolve_tag.outputs.tag }}"
+24 -1
View File
@@ -46,6 +46,8 @@ jobs:
test:
needs: quality-checks
env:
VSCODE_TEST_VERSION: 1.103.0
strategy:
fail-fast: false
matrix:
@@ -81,6 +83,13 @@ jobs:
run: |
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
- name: Cache VS Code test runtime
if: runner.os == 'Windows'
uses: actions/cache@v4
with:
path: .vscode-test
key: vscode-test-runtime-${{ runner.os }}-${{ env.VSCODE_TEST_VERSION }}
# Build the extension and tests (without redundant checks)
- name: Build Tests and Extension
id: build_step
@@ -106,7 +115,21 @@ jobs:
- name: Extension Integration Tests - Non-Linux
id: integration_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: npm run test:integration
run: |
for attempt in 1 2 3; do
echo "Running extension integration tests (attempt ${attempt}/3)"
if npm run test:integration; then
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Extension integration tests failed after 3 attempts"
exit 1
fi
echo "Extension integration tests failed; retrying after short delay"
sleep 5
done
- name: Webview Tests with Coverage
id: webview_tests
+5
View File
@@ -51,3 +51,8 @@ test-results
# Smoke test results (generated)
evals/smoke-tests/results/
.tui-test
secrets.json
tui-traces
tests/**/cache
+2 -1
View File
@@ -3,7 +3,8 @@
"ts"
],
"spec": [
"src/**/__tests__/*.ts"
"src/**/__tests__/*.ts",
"src/test/services/**/*.test.ts"
],
"require": [
"ts-node/register",
+2 -1
View File
@@ -1,5 +1,6 @@
import { defineConfig } from "@vscode/test-cli"
import path from "path"
const vscodeTestVersion = process.env.VSCODE_TEST_VERSION ?? "stable"
export default defineConfig({
files: "{out/**/*.test.js,src/**/*.test.js,!src/test/e2e/**/*.test.js,!out/src/test/e2e/**/*.test.js}",
@@ -12,7 +13,7 @@ export default defineConfig({
require: ["./test-setup.js"],
},
workspaceFolder: "test-workspace",
version: "stable",
version: vscodeTestVersion,
extensionDevelopmentPath: path.resolve("./"),
launchArgs: ["--disable-extensions"],
})
+185
View File
@@ -1,5 +1,190 @@
# Changelog
## [3.82.0]
### Added
- Restore VS Code foreground terminal support and settings.
- Add latest OpenAI, SAP AI Core, and Z AI models.
### Fixed
- Fix hook template JSON escaping.
- Improve ripgrep file search error handling.
### Changed
- Remove hardcoded model lists from docs.
## [3.81.0]
### Added
- Add GPT-5.5 model support for OpenAI Codex subscription users.
### Fixed
- Remove hardcoded "Whats New" fallback items in webview; only remote-configured welcome banners are shown.
### Changed
- Improve cline-core memory diagnostics used by the extension runtime:
- enable near-heap-limit heap snapshots
- add periodic memory usage logging
- log discovered heap snapshots on abnormal exits for easier OOM debugging
## [3.80.0]
### Added
- Wire up remote `globalSkills` from enterprise remote config with full UI, toggle support, and system prompt integration — enterprise-managed skills now appear under a dedicated "Enterprise Skills" section and support `alwaysEnabled` enforcement
- Onboarding flow now uses dynamically fetched recommended models instead of a hardcoded list, with a fallback to the welcome view on failure
- Add dedicated "Quota Exceeded" error message in the chat error UI when Cline account spend caps are hit
### Fixed
- Fix OOM crashes during long conversations by setting `--max-old-space-size=8192` for the cline-core Node.js process (was defaulting to ~2 GB)
- Show detailed error information in the chat error row instead of a generic caught error message
- Update `axios` to 1.15.0 across all packages
### Changed
- Remove foreground terminal mode — all task command execution now defaults to background mode, removing the VS Code integrated terminal dependency and related settings UI
- Remove old hardcoded announcement banners
## [3.79.0]
### Added
- Add Claude Opus 4.7 model support
- Add Azure Blob Storage as a storage provider
- Add `globalSkills` to remote config
- Inline value reuse in user-level remote-config discovery
### Fixed
- Fix cache reflection for Cline and Vercel API handlers
- Fix stuck `command_output` ask when terminal command ends unexpectedly
- Add `use_subagents` to system prompt for GLM, Hermes, and XS models
- Fix action injection security risk
### Changed
- Remove deprecated evals tool
## [3.78.0]
### Added
- Add a dedicated "Spend Limit Reached" error UI when spend caps are hit
- Docs updates
### Fixed
- Show actual `read_file` line ranges in chat UI
## [3.77.0]
### Added
- Add "Lazy Teammate Mode" experimental toggle
- `read_file` tool now supports chunked reading for targeted file access
### Fixed
- Exclude `new_task` tool from system prompt in yolo/headless mode
- Fix Kanban demo video formatting
### Changed
- Polish `Notification` hook functionality
## [3.76.0]
### Added
- Add Cline Kanban launch modal in webview; CLI now launches Kanban by default with a migration view
- Add toggle to disable feature tips in chat
- Add repeated tool call loop detection to prevent infinite loops wasting tokens
### Fixed
- Fix CLI Kanban spawn on Windows by enabling shell mode for `npx.cmd`
## [3.75.0]
### Added
- Latency improvements for remote workspaces
### Fixed
- Stabilize flaky hooks tests
### Changed
- Remove example hooks in favor of reading the docs
## [3.74.0]
### Added
- Implement dynamic free model detection for Cline API
- Add file read deduplication cache to prevent repeated reads
- Add feature tips tooltip during thinking state
### Fixed
- Replace error message when not logged in to Cline
- Align ClineRulesToggleModal padding with ServersToggleModal
- Skip WebP for GLM and Devstral models running through llama.cpp
- Respect user-configured context window in LiteLLM getModel()
- Honor explicit model IDs outside static catalog in W&B provider
- Add missing Fireworks serverless models and pricing
## [3.73.0]
### Added
- Added W&B Inference by CoreWeave as a new API provider with 17 models
- Improved parallel tool calling support for OpenRouter and Cline providers
### Fixed
- Claude Code Provider: handle rate limit events, empty content arrays, error results, and unknown content types without crashing
- Tool handlers (`read_file`, `list_files`, `list_code_definition_names`, `search_files`) now return graceful errors instead of crashing
## [3.72.0]
### Added
- Added Anthropic Opus 4.6 fast mode variants
### Fixed
- Resolved native tool placeholder interpolation in prompts
- Gemini: capped Flash output tokens to 8192 across providers
- Fixed Windows unit test path normalization
- Fixed flaky hooks tests on Windows
- Bedrock: handle thinking and redacted_thinking blocks correctly in message conversion and streaming
- Prevent crash when `list_files` or `list_code_definition_names` receives a file path
### Changed
- Updated Jupyter Notebook GIFs
- Markdown image loading now requires user consent
- Added `.github/copilot-instructions.md` for coding agents
- Hooks: reintroduced feature toggle
## [3.71.0]
### Added
- Added GPT-5.4 models for ChatGPT subscription users
- Hooks: Added a `Notification` hook for attention and completion boundaries
### Fixed
- Handle streamable HTTP MCP reconnects more reliably after disconnects
## [3.70.0]
### Added
-2
View File
@@ -1,3 +1 @@
@.clinerules/general.md
@.clinerules/network.md
@.clinerules/cli.md
-5
View File
@@ -3,11 +3,6 @@ English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md
</sub></div>
# Cline
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
</p>
<div align="center">
<table>
<tbody>
+3 -5
View File
@@ -8,9 +8,7 @@ We actively patch only the most recent minor release of Cline. Older versions re
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.
To report a security issue, please submit your report through our [Bugcrowd Vulnerability Disclosure Program](https://bugcrowd.com/engagements/clinebot-vdp-ess). Bugcrowd will manage communication and triage on our behalf.
When reporting, please include:
@@ -18,10 +16,10 @@ When reporting, please include:
- 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.
Please keep the details private until a resolution has been reached.
## Escalation
If you do not receive an acknowledgement of your report within 5 business days, you may send an email to security@cline.bot.
If you are unable to submit through Bugcrowd, you may send an email to security@cline.bot.
Thank you for helping us keep Cline users safe.
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 512 535">
<!-- Generator: Adobe Illustrator 29.8.5, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
<defs>
<style>
.st0 {
fill: #fff;
}
</style>
</defs>
<path class="st0" d="M500.6,300.5c-9-20.7-17.9-41.4-26.9-62.1-.7-2-.3-4.4-.3-6.4.4-9,1.1-18,1.4-27,2.8-28.4-6.5-58-25.2-79.6-15.1-18.1-36.6-30.7-59.6-35.5-8.1-1.8-16.6-1.6-25-2.1-10-.7-20-1-30-1.7,2-11.9,1-24.1-3.7-35.3-5.8-14.1-16.8-25.9-30.6-32.5-14.4-7-31.5-8.2-46.7-3.1-16,5.2-29.5,17-36.8,32.1-4.9,10-6.8,21.2-6.1,32.2-19.7-1-39.4-2.2-59.1-3.1-26.8.5-53,11.7-72,30.6-20.2,19.5-31.7,47-32.3,75-.5,9.3-1,18.7-1.5,28-.2,2.1,0,4.1-1.2,6-9.8,16.8-19.5,33.7-29.4,50.6-2.2,4.1-4.9,8-6.6,12.3-2,5.7-1.2,12.2,1.3,17.6,8.9,19.5,17.6,39.2,26.5,58.7.8,1.9,1.5,3.7,1.3,5.8-.6,10.3-1.1,20.7-1.7,31-1.5,21.2,3,42.6,13.5,61.1,8.8,15.8,21.6,29.4,37.1,38.9,13.9,8.7,29.7,13.9,46,15.4,72,3.9,144,7.7,216,11.5,20.1,1.8,40.8-2.8,58.5-12.5,18.8-10.1,34.2-26,44.1-44.9,6.5-12.6,10.5-26.4,11.7-40.5.7-12.4,1.2-24.7,2-37.1,0-3.3,1.9-5.5,3.3-8.2,6.6-11.8,13.5-23.4,20.1-35.2,3.7-6.9,8.1-13.4,11.6-20.4,3.2-6.1,3.2-13.5.3-19.7ZM218.5,316.5c-9.7,7.1-21.3,12.3-33.5,12.5-17.6,1-35.1-5.3-49-16-4.6-3.2-8.1-7.5-9.6-13,0-1.8-.7-3.6,1.7-3.8,4,1,7.9,2.6,12,3.5,22.8,5.6,47.6,5.9,71,4.8,6.5-.2,13-1.3,19.5-.9-2.7,5.6-7.1,9.2-12,12.9ZM276,449.7c-14,.5-28,.1-42-.2-2.1,0-4.3,0-6.4-.4-.9-2.1.6-3.2,1.7-4.8,4.8-5.9,11-11,18.7-12.4,8.4-1.6,16.5,1.2,23.5,5.5,4.7,3,9.2,6.3,12.6,10.8-2.6,1.1-5.3,1.4-8.1,1.4ZM390.4,319.4c-16.4,14.2-38.8,21.8-60.4,18.4-13.2-1.6-24.7-8.6-34.1-17.7-3-3-6.2-6.5-8.1-10.4.5-1,1.2-1.6,2.2-1.6,2.8-.2,5.7.7,8.5,1.1,16,2.9,32.3,4.9,48.5,5.5,14.3.4,28.2-.2,42.2-3.6,2.2-.6,3.7-.3,5.8.5-1.1,2.9-2.2,5.7-4.6,7.8Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+1 -3
View File
@@ -179,9 +179,7 @@
"!!**/*.js",
"!!**/scripts/**",
"!!**/*.tsx",
"!!**/testing-platform/**",
// ACP mode must redirect console to stderr - this is intentional
"!!cli/src/acp/index.ts"
"!!**/testing-platform/**"
]
},
{
+157
View File
@@ -1,5 +1,162 @@
# cline
## [2.18.0]
### Added
- Restore foreground terminal support and settings.
- Add latest OpenAI, SAP AI Core, and Z AI models.
### Fixed
- Fix hook template JSON escaping.
- Improve ripgrep file search error handling.
### Changed
- Remove hardcoded model lists from docs.
## [2.17.0]
### Added
- Add GPT-5.5 model support for OpenAI Codex subscription users.
### Changed
- Improve `cline-core` runtime memory diagnostics used by CLI:
- enable near-heap-limit heap snapshots
- add periodic memory usage logging
- log discovered heap snapshots on abnormal exits for easier OOM debugging
## [2.16.0]
### Added
- Wire up remote `globalSkills` from enterprise remote config with full toggle support and system prompt integration — enterprise-managed skills now support `alwaysEnabled` enforcement
- Add dedicated "Quota Exceeded" error message when Cline account spend caps are hit
### Fixed
- Fix OOM crashes during long conversations by setting `--max-old-space-size=8192` for the cline-core Node.js process (was defaulting to ~2 GB)
- Show detailed error information instead of a generic caught error message
- Update `axios` to 1.15.0 across all packages
### Changed
- Remove dead ACP terminal setter stubs as part of foreground terminal mode removal
## [2.15.0]
### Added
- Add Claude Opus 4.7 model support
- Inline value reuse in user-level remote-config discovery
- Add `globalSkills` to remote config
### Fixed
- Stabilize Windows CI test path handling
## [2.14.0]
### Added
- Simplify unified `cline update` flow for `cline` and `kanban`
- Docs updates
### Fixed
- Update Kanban migration view copy
## [2.12.0]
### Added
- `read_file` tool now supports chunked reading for targeted file access
### Fixed
- Exclude `new_task` tool from system prompt in yolo/headless mode
### Changed
- Polish `Notification` hook functionality
## [2.9.0]
### Added
- Latency improvements for remote workspaces
## [2.8.2]
### Fixed
- Use `kanban@latest` in `cline kanban` to always fetch the newest version
## [2.8.1]
### Added
- Implement dynamic free model detection for Cline API
- Add file read deduplication cache to prevent repeated reads
- Add feature tips tooltip during thinking state
### Fixed
- Fix flaky CLI Enter-key handling across Windows/test environments
- Replace error message when not logged in to Cline
- Align ClineRulesToggleModal padding with ServersToggleModal
- Skip WebP for GLM and Devstral models running through llama.cpp
- Respect user-configured context window in LiteLLM getModel()
- Honor explicit model IDs outside static catalog in W&B provider
- Add missing Fireworks serverless models and pricing
## [2.8.0]
### Added
- Added W&B Inference by CoreWeave as a new API provider with 17 models including DeepSeek-V3.1, Llama 4, and Qwen3-Coder
- Added CLI TUI end-to-end test suite
### Fixed
- Claude Code: handle rate limit events, empty content arrays, error results, and unknown content types without crashing
- CLI: `/q` and `/exit` slash commands now execute immediately on Enter without requiring the slash menu to be visible
- CLI: slash command filtering now prioritizes exact and prefix matches over fuzzy matches
## [2.7.0]
### Added
- Added MCP add shortcuts for stdio and HTTP servers
- Added `--continue` for the current directory
- Added `--auto-condense` flag for AI-powered context compaction
- Added `--hooks-dir` flag for runtime hook injection
- Enabled error autocapture
- Prompt rules now include test verification guidance and make `CLI_RULES` language-agnostic
### Fixed
- Fixed remount behavior so TUI remounts only on width resize
- Fixed startup prompt replay on resize remount
- Fixed task flags so they are applied before the welcome TUI mounts
### Changed
- Hooks: reintroduced feature toggle
## [2.6.1]
### Added
- Added GPT-5.4 models for ChatGPT subscription users
- Hooks: Added a `Notification` hook for attention and completion boundaries
- Added `--hooks-dir` CLI flag for runtime hook injection
- Added `--auto-approve-all` CLI flag for interactive mode
### Fixed
- Handle streamable HTTP MCP reconnects more reliably
## [2.6.0]
### Added
+1
View File
@@ -186,6 +186,7 @@ const buildEnvVars: Record<string, string> = {
const buildTimeEnvs = [
"TELEMETRY_SERVICE_API_KEY",
"ERROR_SERVICE_API_KEY",
"ENABLE_ERROR_AUTOCAPTURE",
"POSTHOG_TELEMETRY_ENABLED",
"OTEL_TELEMETRY_ENABLED",
"OTEL_LOGS_EXPORTER",
+5
View File
@@ -162,6 +162,8 @@ When running **cline** with just a prompt (no subcommand), these options are ava
**-T**, **\--taskId** *id* : Resume an existing task by ID instead of starting a new one. The prompt becomes an optional follow-up message.
**\--continue** : Resume the most recent task from the current working directory instead of starting a new one.
# JSON OUTPUT FORMAT
When using **\--json**, each message is output as a JSON object with these fields:
@@ -268,6 +270,9 @@ cline -T abc123def
# Resume a task with a follow-up message
cline -T abc123def "Now add unit tests for the changes"
# Resume the most recent task from the current directory
cline --continue
# Resume in plan mode to review before continuing
cline -T abc123def -p "What's left to do?"
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "cline",
"version": "2.6.0",
"version": "2.18.0",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"main": "dist/lib.mjs",
"types": "dist/lib.d.ts",
@@ -32,7 +32,7 @@
"package": "npm pack --pack-destination ./dist",
"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",
"build:types": "(npx tsc -p tsconfig.lib.json || true) && cp dist/types/cli/src/exports.d.ts dist/lib.d.ts && rm -rf dist/types dist/agent",
"watch": "npx tsx esbuild.mts --watch",
"dev": "IS_DEV=true && npm run link && npm run watch ; npm run unlink",
"clean": "rimraf dist",
@@ -82,7 +82,6 @@
"vitest": "^4.0.17"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.13.1",
"@vscode/ripgrep": "^1.15.9",
"aws4fetch": "^1.0.20",
"chalk": "^5.3.0",
-248
View File
@@ -1,248 +0,0 @@
/**
* ACP-based implementation of DiffViewProvider that uses the ACP client's
* filesystem capabilities for reading and writing files.
*
* This provider attempts to use the ACP client's fs/read_text_file and
* fs/write_text_file methods when available, falling back to the
* FileEditProvider's local filesystem implementation otherwise.
*
* @module acp
*/
import type * as acp from "@agentclientprotocol/sdk"
import { workspaceResolver } from "@core/workspace"
import { createDirectoriesForFile } from "@utils/fs"
import { getCwd } from "@utils/path"
import * as fs from "fs/promises"
import * as iconv from "iconv-lite"
import { HostProvider } from "@/hosts/host-provider"
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
import { detectEncoding } from "@/integrations/misc/extract-text"
import type { FileDiagnostics } from "@/shared/proto/index.cline"
import { Logger } from "@/shared/services/Logger"
/**
* A function that resolves the current session ID.
* This is used by ACPDiffViewProvider to get the session ID at runtime,
* since the provider may be created before a session exists.
*/
export type SessionIdResolver = () => string | undefined
/**
* A DiffViewProvider implementation that uses the ACP client's filesystem
* capabilities when available, with fallback to local filesystem operations.
*
* This class extends FileEditProvider and overrides the file I/O methods to
* use the ACP protocol's fs/read_text_file and fs/write_text_file requests
* when the client supports these capabilities. This allows the editor (client)
* to handle file operations, which enables features like:
* - Reading unsaved editor state
* - Tracking file modifications in the editor
* - Proper integration with the client's undo/redo stack
*/
export class ACPDiffViewProvider extends FileEditProvider {
private readonly connection: acp.AgentSideConnection
private readonly clientCapabilities: acp.ClientCapabilities | undefined
private readonly sessionIdResolver: SessionIdResolver
/**
* Creates a new ACPDiffViewProvider.
*
* @param connection - The ACP agent-side connection for making requests
* @param clientCapabilities - The client's advertised capabilities
* @param sessionIdResolver - A function that returns the current session ID
*/
constructor(
connection: acp.AgentSideConnection,
clientCapabilities: acp.ClientCapabilities | undefined,
sessionIdResolver: SessionIdResolver,
) {
super()
this.connection = connection
this.clientCapabilities = clientCapabilities
this.sessionIdResolver = sessionIdResolver
}
/**
* Gets the current session ID, or throws if no session is active.
*/
private getSessionId(): string {
const sessionId = this.sessionIdResolver()
if (!sessionId) {
throw new Error("No active ACP session. Cannot perform file operation.")
}
return sessionId
}
/**
* Check if the client supports file read operations.
*/
private canReadFile(): boolean {
return this.clientCapabilities?.fs?.readTextFile === true
}
/**
* Check if the client supports file write operations.
*/
private canWriteFile(): boolean {
return this.clientCapabilities?.fs?.writeTextFile === true
}
/**
* Opens a file for editing, using ACP fs capabilities when available.
*
* If the client supports fs/read_text_file, this method will read the file
* content via the ACP connection, which may include unsaved editor state.
* Otherwise, it falls back to the FileEditProvider's local fs implementation.
*/
override async open(relPath: string, options?: { displayPath?: string }): Promise<void> {
// If we can't read files via ACP, fall back to FileEditProvider
if (!this.canReadFile()) {
Logger.debug("[ACPDiffViewProvider] Client does not support fs.readTextFile, falling back to local fs")
return super.open(relPath, options)
}
// Set up state - this replicates the DiffViewProvider.open() logic
// but uses ACP for file reading instead of local fs
this.isEditing = true
const cwd = await getCwd()
const absolutePathResolved = workspaceResolver.resolveWorkspacePath(cwd, relPath, "ACPDiffViewProvider.open.absolutePath")
this.absolutePath = typeof absolutePathResolved === "string" ? absolutePathResolved : absolutePathResolved.absolutePath
this.relPath = options?.displayPath ?? relPath
const fileExists = this.editType === "modify"
// Read file content
if (fileExists) {
// Try to save any dirty state in the editor first
try {
await HostProvider.workspace.saveOpenDocumentIfDirty({
filePath: this.absolutePath!,
})
} catch {
// Ignore errors - the host may not support this
}
// Read file content via ACP
try {
Logger.debug("[ACPDiffViewProvider] Reading file via ACP:", this.absolutePath)
const response = await this.connection.readTextFile({
sessionId: this.getSessionId(),
path: this.absolutePath!,
})
this.originalContent = response.content
// ACP always returns UTF-8 text content
this.fileEncoding = "utf8"
Logger.debug("[ACPDiffViewProvider] Read file successfully, length:", response.content.length)
} catch (error) {
// If ACP read fails, fall back to local fs
Logger.debug("[ACPDiffViewProvider] ACP read failed, falling back to local fs:", error)
const fileBuffer = await fs.readFile(this.absolutePath!)
this.fileEncoding = await detectEncoding(fileBuffer)
this.originalContent = iconv.decode(fileBuffer, this.fileEncoding)
}
} else {
this.originalContent = ""
this.fileEncoding = "utf8"
}
// Create directories for new files
const createdDirs = await createDirectoriesForFile(this.absolutePath!)
// Store for potential cleanup - access via the private field workaround
;(this as any).createdDirs = createdDirs
// Make sure the file exists before we proceed
if (!fileExists) {
// For new files, write via ACP if possible, otherwise local fs
if (this.canWriteFile()) {
try {
await this.connection.writeTextFile({
sessionId: this.getSessionId(),
path: this.absolutePath!,
content: "",
})
} catch {
// Fall back to local fs
await fs.writeFile(this.absolutePath!, "")
}
} else {
await fs.writeFile(this.absolutePath!, "")
}
}
// Get diagnostics before editing
let preDiagnostics: FileDiagnostics[] = []
try {
preDiagnostics = (await HostProvider.workspace.getDiagnostics({})).fileDiagnostics
} catch {
preDiagnostics = []
}
;(this as any).preDiagnostics = preDiagnostics
// Call the parent's openDiffEditor to set up in-memory document content
await this.openDiffEditor()
await this.scrollEditorToLine(0)
;(this as any).streamedLines = []
}
/**
* Scrolls the editor to a specific line.
* No-op for file-based providers, but needed for protected access.
*/
protected override async scrollEditorToLine(_line: number): Promise<void> {
// No-op: No visual editor to scroll
}
/**
* Opens the diff editor.
*/
protected override async openDiffEditor(): Promise<void> {
// Set up in-memory document content from the original content
// no-op: No visual editor to open
}
/**
* Saves the document content, using ACP fs capabilities when available.
*
* If the client supports fs/write_text_file, this method will write the file
* content via the ACP connection. Otherwise, it falls back to the
* FileEditProvider's local fs implementation.
*/
protected override async saveDocument(): Promise<Boolean> {
// If we can't write files via ACP, fall back to FileEditProvider
if (!this.canWriteFile()) {
Logger.debug("[ACPDiffViewProvider] Client does not support fs.writeTextFile, falling back to local fs")
return super.saveDocument()
}
const content = await this.getContent()
if (!this.absolutePath || content === undefined) {
return false
}
try {
Logger.debug("[ACPDiffViewProvider] Writing file via ACP:", {
path: this.absolutePath,
contentLength: content.length,
})
await this.connection.writeTextFile({
sessionId: this.getSessionId(),
path: this.absolutePath,
content: content,
})
Logger.debug("[ACPDiffViewProvider] Write file successfully")
return true
} catch (error) {
// If ACP write fails, fall back to local fs
Logger.debug("[ACPDiffViewProvider] ACP write failed, falling back to local fs:", error)
return super.saveDocument()
}
}
}
-408
View File
@@ -1,408 +0,0 @@
/**
* ACP Host Bridge Client Provider
*
* Implements HostBridgeClientProvider for ACP mode, providing stub implementations
* of the 4 required service clients. These clients conform to the interfaces in
* host-bridge-client-types.ts and will use ACP connection capabilities where applicable.
*
* @module acp
*/
import type * as acp from "@agentclientprotocol/sdk"
import type {
DiffServiceClientInterface,
EnvServiceClientInterface,
WindowServiceClientInterface,
WorkspaceServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
import type { HostBridgeClientProvider, StreamingCallbacks } from "@hosts/host-provider-types"
import * as proto from "@shared/proto/index"
import { ClineClient } from "@/shared/cline"
import { Logger } from "@/shared/services/Logger"
/**
* Function type that resolves the current session ID.
* Returns undefined if no session is active.
*/
export type SessionIdResolver = () => string | undefined
/**
* Function type that resolves the current working directory.
* Returns undefined if no cwd is available (will fall back to process.cwd()).
*/
export type CwdResolver = () => string | undefined
/**
* ACP implementation of DiffService client.
*
* Handles diff operations for the ACP environment. Most operations are stubs
* that will be implemented in the next phase using ACP extension methods or
* the fs capabilities (readTextFile/writeTextFile).
*/
class ACPDiffServiceClient implements DiffServiceClientInterface {
async openDiff(_request: proto.host.OpenDiffRequest): Promise<proto.host.OpenDiffResponse> {
// Next phase: Could use ACP client capabilities to open a diff view in the editor.
// This would involve sending an ACP extension notification/request to the client
// to display a side-by-side diff of the original vs modified content.
Logger.debug("[ACPDiffServiceClient] openDiff called (stub)")
return proto.host.OpenDiffResponse.create({})
}
async getDocumentText(request: proto.host.GetDocumentTextRequest): Promise<proto.host.GetDocumentTextResponse> {
// Next phase: Use connection.readTextFile if clientCapabilities.fs.readTextFile is available.
// This would read the current document content from the editor, including any unsaved changes.
// For now, return empty content.
Logger.debug("[ACPDiffServiceClient] getDocumentText called (stub)", { diffId: request.diffId })
return proto.host.GetDocumentTextResponse.create({ content: "" })
}
async replaceText(_request: proto.host.ReplaceTextRequest): Promise<proto.host.ReplaceTextResponse> {
// Next phase: Use connection.writeTextFile if clientCapabilities.fs.writeTextFile is available.
// This would replace text in the document at the specified range.
Logger.debug("[ACPDiffServiceClient] replaceText called (stub)")
return proto.host.ReplaceTextResponse.create({})
}
async scrollDiff(_request: proto.host.ScrollDiffRequest): Promise<proto.host.ScrollDiffResponse> {
// Next phase: Send ACP extension notification to scroll the diff view to a specific line.
// No visual editor in ACP mode by default, so this is a no-op.
Logger.debug("[ACPDiffServiceClient] scrollDiff called (stub)")
return proto.host.ScrollDiffResponse.create({})
}
async truncateDocument(_request: proto.host.TruncateDocumentRequest): Promise<proto.host.TruncateDocumentResponse> {
// Next phase: Read file using readTextFile, truncate content, write back using writeTextFile.
// This is used to truncate a document to a specific line count.
Logger.debug("[ACPDiffServiceClient] truncateDocument called (stub)")
return proto.host.TruncateDocumentResponse.create({})
}
async saveDocument(_request: proto.host.SaveDocumentRequest): Promise<proto.host.SaveDocumentResponse> {
// Next phase: Use connection.writeTextFile to persist the document to disk.
// This saves the current document content to the file system.
Logger.debug("[ACPDiffServiceClient] saveDocument called (stub)")
return proto.host.SaveDocumentResponse.create({})
}
async closeAllDiffs(_request: proto.host.CloseAllDiffsRequest): Promise<proto.host.CloseAllDiffsResponse> {
// Next phase: Send ACP extension notification to close all diff views in the editor.
// No visual diff views in ACP mode by default, so this is a no-op.
Logger.debug("[ACPDiffServiceClient] closeAllDiffs called (stub)")
return proto.host.CloseAllDiffsResponse.create({})
}
async openMultiFileDiff(_request: proto.host.OpenMultiFileDiffRequest): Promise<proto.host.OpenMultiFileDiffResponse> {
// Next phase: Send ACP extension notification to open a multi-file diff view.
// This would display changes across multiple files in the editor.
Logger.debug("[ACPDiffServiceClient] openMultiFileDiff called (stub)")
return proto.host.OpenMultiFileDiffResponse.create({})
}
}
/**
* ACP implementation of EnvService client.
*
* Handles environment operations like clipboard access, version info, and telemetry.
* Most operations are stubs that will be implemented using ACP extension methods.
*/
class ACPEnvServiceClient implements EnvServiceClientInterface {
private readonly version: string
constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver, version: string) {
this.version = version
}
async debugLog(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
Logger.debug(request.value)
return proto.cline.Empty.create()
}
async clipboardWriteText(_request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
Logger.debug("[ACPEnvServiceClient] clipboardWriteText called (stub)")
return proto.cline.Empty.create()
}
async clipboardReadText(_request: proto.cline.EmptyRequest): Promise<proto.cline.String> {
Logger.debug("[ACPEnvServiceClient] clipboardReadText called (stub)")
return proto.cline.String.create({ value: "" })
}
async getHostVersion(_request: proto.cline.EmptyRequest): Promise<proto.host.GetHostVersionResponse> {
// Return version info for the ACP agent.
return proto.host.GetHostVersionResponse.create({
version: this.version,
platform: "Cline ACP Agent",
clineType: ClineClient.Cli,
})
}
async getIdeRedirectUri(_request: proto.cline.EmptyRequest): Promise<proto.cline.String> {
Logger.debug("[ACPEnvServiceClient] getIdeRedirectUri called (stub)")
return proto.cline.String.create({ value: "" })
}
async getTelemetrySettings(_request: proto.cline.EmptyRequest): Promise<proto.host.GetTelemetrySettingsResponse> {
// Return telemetry as disabled by default in ACP mode.
return proto.host.GetTelemetrySettingsResponse.create({
isEnabled: proto.host.Setting.DISABLED,
})
}
subscribeToTelemetrySettings(
_request: proto.cline.EmptyRequest,
callbacks: StreamingCallbacks<proto.host.TelemetrySettingsEvent>,
): () => void {
// Send initial telemetry settings (disabled) and return unsubscribe function.
callbacks.onResponse(
proto.host.TelemetrySettingsEvent.create({
isEnabled: proto.host.Setting.DISABLED,
}),
)
// Return no-op unsubscribe function
return () => {}
}
async shutdown(_request: proto.cline.EmptyRequest): Promise<proto.cline.Empty> {
// Next phase: Graceful ACP connection shutdown.
// This would cleanly close the ACP connection and release resources.
Logger.debug("[ACPEnvServiceClient] shutdown called (stub)")
return proto.cline.Empty.create()
}
async openExternal(request: proto.cline.StringRequest): Promise<proto.cline.Empty> {
const url = request.value || ""
if (url) {
Logger.debug(`[ACPEnvServiceClient] openExternal: ${url}`)
const { openUrlInBrowser } = await import("../utils/browser")
await openUrlInBrowser(url)
}
return proto.cline.Empty.create()
}
}
/**
* ACP implementation of WindowService client.
*
* Handles window/UI operations like showing documents, dialogs, and messages.
* Most operations are stubs that will be implemented using ACP extension methods.
*/
class ACPWindowServiceClient implements WindowServiceClientInterface {
constructor(_clientCapabilities: acp.ClientCapabilities | undefined, _sessionIdResolver: SessionIdResolver) {}
async showTextDocument(request: proto.host.ShowTextDocumentRequest): Promise<proto.host.TextEditorInfo> {
// Next phase: Send ACP extension request to open document in the editor.
// This would tell the ACP client to open the specified file.
Logger.debug("[ACPWindowServiceClient] showTextDocument called (stub)", { path: request.path })
return proto.host.TextEditorInfo.create({
documentPath: request.path,
})
}
async showOpenDialogue(_request: proto.host.ShowOpenDialogueRequest): Promise<proto.host.SelectedResources> {
// Next phase: Send ACP extension request for file picker dialog.
// This would display a file open dialog in the ACP client.
Logger.debug("[ACPWindowServiceClient] showOpenDialogue called (stub)")
return proto.host.SelectedResources.create({ paths: [] })
}
async showMessage(request: proto.host.ShowMessageRequest): Promise<proto.host.SelectedResponse> {
// Next phase: Send ACP extension notification to show message in the editor.
// This would display an information/warning/error message to the user.
Logger.debug("[ACPWindowServiceClient] showMessage called (stub)", {
message: request.message,
type: request.type,
})
return proto.host.SelectedResponse.create({})
}
async showInputBox(_request: proto.host.ShowInputBoxRequest): Promise<proto.host.ShowInputBoxResponse> {
// Next phase: Send ACP extension request for input dialog.
// This would display an input box for user text entry.
Logger.debug("[ACPWindowServiceClient] showInputBox called (stub)")
return proto.host.ShowInputBoxResponse.create({ response: "" })
}
async showSaveDialog(_request: proto.host.ShowSaveDialogRequest): Promise<proto.host.ShowSaveDialogResponse> {
// Next phase: Send ACP extension request for save dialog.
// This would display a file save dialog in the ACP client.
Logger.debug("[ACPWindowServiceClient] showSaveDialog called (stub)")
return proto.host.ShowSaveDialogResponse.create({ selectedPath: "" })
}
async openFile(request: proto.host.OpenFileRequest): Promise<proto.host.OpenFileResponse> {
// Next phase: Send ACP extension request to open file in the editor.
// This would open the specified file in the ACP client's editor.
Logger.debug("[ACPWindowServiceClient] openFile called (stub)", { filePath: request.filePath })
return proto.host.OpenFileResponse.create({})
}
async openSettings(_request: proto.host.OpenSettingsRequest): Promise<proto.host.OpenSettingsResponse> {
// Next phase: Send ACP extension request to open settings panel.
// This would open the settings/preferences in the ACP client.
Logger.debug("[ACPWindowServiceClient] openSettings called (stub)")
return proto.host.OpenSettingsResponse.create({})
}
async getOpenTabs(_request: proto.host.GetOpenTabsRequest): Promise<proto.host.GetOpenTabsResponse> {
// Next phase: Send ACP extension request to list open tabs/documents.
// This would return a list of currently open files in the editor.
Logger.debug("[ACPWindowServiceClient] getOpenTabs called (stub)")
return proto.host.GetOpenTabsResponse.create({ paths: [] })
}
async getVisibleTabs(_request: proto.host.GetVisibleTabsRequest): Promise<proto.host.GetVisibleTabsResponse> {
// Next phase: Send ACP extension request to list visible tabs.
// This would return a list of visible tabs/panes in the editor.
Logger.debug("[ACPWindowServiceClient] getVisibleTabs called (stub)")
return proto.host.GetVisibleTabsResponse.create({ paths: [] })
}
async getActiveEditor(_request: proto.host.GetActiveEditorRequest): Promise<proto.host.GetActiveEditorResponse> {
// Next phase: Send ACP extension request to get active editor info.
// This would return information about the currently focused editor.
Logger.debug("[ACPWindowServiceClient] getActiveEditor called (stub)")
return proto.host.GetActiveEditorResponse.create({})
}
}
/**
* ACP implementation of WorkspaceService client.
*
* Handles workspace operations like getting paths, diagnostics, and terminal commands.
* Uses the cwdResolver to get the current working directory, falling back to process.cwd().
*/
class ACPWorkspaceServiceClient implements WorkspaceServiceClientInterface {
private readonly _clientCapabilities: acp.ClientCapabilities | undefined
private readonly cwdResolver: CwdResolver
constructor(
clientCapabilities: acp.ClientCapabilities | undefined,
_sessionIdResolver: SessionIdResolver,
cwdResolver: CwdResolver,
) {
this._clientCapabilities = clientCapabilities
this.cwdResolver = cwdResolver
}
/**
* Get the current working directory, using the resolver if available,
* otherwise falling back to process.cwd().
*/
private getCwd(): string {
return this.cwdResolver() ?? process.cwd()
}
async getWorkspacePaths(_request: proto.host.GetWorkspacePathsRequest): Promise<proto.host.GetWorkspacePathsResponse> {
// Return the current working directory from the resolver.
const cwd = this.getCwd()
Logger.debug("[ACPWorkspaceServiceClient] getWorkspacePaths called", { cwd })
return proto.host.GetWorkspacePathsResponse.create({
paths: [cwd],
})
}
async saveOpenDocumentIfDirty(
_request: proto.host.SaveOpenDocumentIfDirtyRequest,
): Promise<proto.host.SaveOpenDocumentIfDirtyResponse> {
// Next phase: Use ACP extension or fs.writeTextFile to save dirty documents.
// This would save any unsaved changes in the specified document.
Logger.debug("[ACPWorkspaceServiceClient] saveOpenDocumentIfDirty called (stub)")
return proto.host.SaveOpenDocumentIfDirtyResponse.create({})
}
async getDiagnostics(_request: proto.host.GetDiagnosticsRequest): Promise<proto.host.GetDiagnosticsResponse> {
// Next phase: Send ACP extension request for diagnostics (errors, warnings).
// This would return linting/compilation errors from the ACP client.
Logger.debug("[ACPWorkspaceServiceClient] getDiagnostics called (stub)")
return proto.host.GetDiagnosticsResponse.create({ fileDiagnostics: [] })
}
async openProblemsPanel(_request: proto.host.OpenProblemsPanelRequest): Promise<proto.host.OpenProblemsPanelResponse> {
// Next phase: Send ACP extension notification to open the problems panel.
// This would show the diagnostics/problems view in the editor.
Logger.debug("[ACPWorkspaceServiceClient] openProblemsPanel called (stub)")
return proto.host.OpenProblemsPanelResponse.create({})
}
async openInFileExplorerPanel(
request: proto.host.OpenInFileExplorerPanelRequest,
): Promise<proto.host.OpenInFileExplorerPanelResponse> {
// Next phase: Send ACP extension notification to reveal file in explorer.
// This would highlight/reveal the specified path in the file tree.
Logger.debug("[ACPWorkspaceServiceClient] openInFileExplorerPanel called (stub)", { path: request.path })
return proto.host.OpenInFileExplorerPanelResponse.create({})
}
async openClineSidebarPanel(
_request: proto.host.OpenClineSidebarPanelRequest,
): Promise<proto.host.OpenClineSidebarPanelResponse> {
// Next phase: Send ACP extension notification to open Cline sidebar.
// This would show the Cline panel/sidebar in the editor.
Logger.debug("[ACPWorkspaceServiceClient] openClineSidebarPanel called (stub)")
return proto.host.OpenClineSidebarPanelResponse.create({})
}
async openTerminalPanel(_request: proto.host.OpenTerminalRequest): Promise<proto.host.OpenTerminalResponse> {
// Next phase: Send ACP extension notification or use createTerminal capability.
// This would open/show the terminal panel in the editor.
Logger.debug("[ACPWorkspaceServiceClient] openTerminalPanel called (stub)")
return proto.host.OpenTerminalResponse.create({})
}
async executeCommandInTerminal(
request: proto.host.ExecuteCommandInTerminalRequest,
): Promise<proto.host.ExecuteCommandInTerminalResponse> {
// Next phase: Use connection.createTerminal if clientCapabilities.terminal is available.
// This would execute the specified command in a terminal via the ACP client.
// The ACP SDK provides createTerminal() which returns a TerminalHandle with
// methods like currentOutput(), waitForExit(), kill(), and release().
Logger.debug("[ACPWorkspaceServiceClient] executeCommandInTerminal called (stub)", {
command: request.command,
hasTerminalCapability: this._clientCapabilities?.terminal,
})
return proto.host.ExecuteCommandInTerminalResponse.create({})
}
async openFolder(request: proto.host.OpenFolderRequest): Promise<proto.host.OpenFolderResponse> {
// Next phase: Send ACP extension request to change workspace/folder.
// This would open a new folder/workspace in the ACP client.
Logger.debug("[ACPWorkspaceServiceClient] openFolder called (stub)", { path: request.path })
return proto.host.OpenFolderResponse.create({ success: true })
}
}
/**
* ACP Host Bridge Client Provider
*
* Provides the 4 service clients required by HostBridgeClientProvider interface,
* implemented for the ACP environment. Uses the ACP connection and client capabilities
* to delegate operations to the ACP client where possible.
*/
export class ACPHostBridgeClientProvider implements HostBridgeClientProvider {
workspaceClient: WorkspaceServiceClientInterface
envClient: EnvServiceClientInterface
windowClient: WindowServiceClientInterface
diffClient: DiffServiceClientInterface
/**
* Creates a new ACPHostBridgeClientProvider.
*
* @param connection - The ACP agent-side connection for making requests
* @param clientCapabilities - The client's advertised capabilities
* @param sessionIdResolver - Function that returns the current session ID
* @param cwdResolver - Function that returns the current working directory
* @param debug - Whether to enable debug logging
* @param version - Version string for getHostVersion (optional)
*/
constructor(
clientCapabilities: acp.ClientCapabilities | undefined,
sessionIdResolver: SessionIdResolver,
cwdResolver: CwdResolver,
version: string,
) {
this.workspaceClient = new ACPWorkspaceServiceClient(clientCapabilities, sessionIdResolver, cwdResolver)
this.envClient = new ACPEnvServiceClient(clientCapabilities, sessionIdResolver, version)
this.windowClient = new ACPWindowServiceClient(clientCapabilities, sessionIdResolver)
this.diffClient = new ACPDiffServiceClient()
}
}
-141
View File
@@ -1,141 +0,0 @@
/**
* AcpAgent - Thin wrapper that bridges stdio connection to ClineAgent.
*
* This class wraps the ClineAgent and connects it to an ACP AgentSideConnection
* for stdio-based communication. It:
* - Wires up the permission handler to call connection.requestPermission()
* - Subscribes to ClineAgent session events and forwards them to connection.sessionUpdate()
* - Delegates all acp.Agent methods to the internal ClineAgent
*
* For programmatic usage without stdio, use ClineAgent directly.
*
* @module acp
*/
import type * as acp from "@agentclientprotocol/sdk"
import { Logger } from "@/shared/services/Logger.js"
import { ClineAgent } from "../agent/ClineAgent.js"
import { type AcpAgentOptions, type SessionUpdateType } from "../agent/types.js"
/**
* ACP Agent wrapper that bridges stdio connection to ClineAgent.
*
* This is the class used by runAcpMode() for stdio-based ACP communication.
* It creates an internal ClineAgent and wires up the connection for:
* - Permission requests (via connection.requestPermission)
* - Session updates (via connection.sessionUpdate)
*/
export class AcpAgent implements acp.Agent {
private readonly connection: acp.AgentSideConnection
private readonly clineAgent: ClineAgent
/** Track which sessions we've subscribed to for event forwarding */
private readonly subscribedSessions: Set<string> = new Set()
constructor(connection: acp.AgentSideConnection, options: AcpAgentOptions) {
this.connection = connection
// Create the internal ClineAgent
this.clineAgent = new ClineAgent(options)
// Wire up the permission handler to use the connection
this.clineAgent.setPermissionHandler(async (request) => {
try {
Logger.debug("[AcpAgent] Forwarding permission request to connection")
return await this.connection.requestPermission({
sessionId: request.sessionId,
toolCall: request.toolCall,
options: request.options,
})
} catch (error) {
Logger.debug("[AcpAgent] Error requesting permission:", error)
return { outcome: { outcome: "cancelled" } }
}
})
}
/**
* Subscribe to session events and forward them to the connection.
*/
private subscribeToSessionEvents(sessionId: string): void {
if (this.subscribedSessions.has(sessionId)) {
return
}
const emitter = this.clineAgent.emitterForSession(sessionId)
// Forward session update by adding the sessionUpdate discriminator
const forwardSessionUpdate = <K extends SessionUpdateType>(eventName: K) => {
emitter.on(eventName, (payload: Record<string, unknown>) => {
const update = {
sessionUpdate: eventName,
...payload,
} as acp.SessionUpdate
this.connection.sessionUpdate({ sessionId, update }).catch((error) => {
Logger.error(`[AcpAgent] Error forwarding ${eventName}:`, error)
})
})
}
// Forward all standard session updates
forwardSessionUpdate("agent_message_chunk")
forwardSessionUpdate("agent_thought_chunk")
forwardSessionUpdate("tool_call")
forwardSessionUpdate("tool_call_update")
forwardSessionUpdate("available_commands_update")
forwardSessionUpdate("plan")
forwardSessionUpdate("current_mode_update")
forwardSessionUpdate("user_message_chunk")
forwardSessionUpdate("config_option_update")
forwardSessionUpdate("session_info_update")
// Handle errors specially (not part of ACP SessionUpdate)
emitter.on("error", (error) => {
Logger.error("[AcpAgent] Session error:", error)
})
this.subscribedSessions.add(sessionId)
}
// ============================================================
// acp.Agent Interface Implementation - Delegate to ClineAgent
// ============================================================
async initialize(params: acp.InitializeRequest): Promise<acp.InitializeResponse> {
return await this.clineAgent.initialize(params, this.connection)
}
async newSession(params: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
const response = await this.clineAgent.newSession(params)
// Subscribe to events for this new session
this.subscribeToSessionEvents(response.sessionId)
return response
}
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
// Ensure we're subscribed to this session's events
this.subscribeToSessionEvents(params.sessionId)
return this.clineAgent.prompt(params)
}
async cancel(params: acp.CancelNotification): Promise<void> {
return this.clineAgent.cancel(params)
}
async setSessionMode(params: acp.SetSessionModeRequest): Promise<acp.SetSessionModeResponse> {
return this.clineAgent.setSessionMode(params)
}
async unstable_setSessionModel(params: acp.SetSessionModelRequest): Promise<acp.SetSessionModelResponse> {
return this.clineAgent.unstable_setSessionModel(params)
}
async authenticate(params: acp.AuthenticateRequest): Promise<acp.AuthenticateResponse> {
return this.clineAgent.authenticate(params)
}
async shutdown(): Promise<void> {
this.subscribedSessions.clear()
return this.clineAgent.shutdown()
}
}
File diff suppressed because it is too large Load Diff
-138
View File
@@ -1,138 +0,0 @@
/**
* Entry point for ACP (Agent Client Protocol) mode.
*
* When the CLI is invoked with `--acp`, this module sets up the ACP connection
* and runs Cline as an ACP-compliant agent communicating over stdio.
*
* This module exports:
* - `ClineAgent` - Decoupled agent for programmatic use (no stdio dependency)
* - `AcpAgent` - Thin wrapper that bridges stdio connection to ClineAgent
* - `ClineSessionEmitter` - Typed EventEmitter for per-session events
* - `runAcpMode` - Function to run Cline in stdio-based ACP mode
*
* @module acp
*/
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
import { Logger } from "@/shared/services/Logger"
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"
export type {
AcpAgentOptions,
AcpSessionState,
ClineAgentOptions,
ClineSessionEvents,
PermissionHandler,
} from "../agent/types.js"
export { AcpAgent } from "./AcpAgent.js"
/** Original console methods for restoration if needed */
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
debug: console.debug,
error: console.error,
}
/**
* Redirect all console output to stderr.
*
* In ACP mode, stdout is reserved exclusively for JSON-RPC communication.
* All logging must go to stderr to avoid corrupting the protocol stream.
*/
function redirectConsoleToStderr(): void {
console.log = (...args) => console.error(...args)
console.info = (...args) => console.error(...args)
console.warn = (...args) => console.error(...args)
console.debug = (...args) => console.error(...args)
// console.error already goes to stderr
}
/**
* Restore console methods to their original behavior.
*/
export function restoreConsole(): void {
console.log = originalConsole.log
console.info = originalConsole.info
console.warn = originalConsole.warn
console.debug = originalConsole.debug
console.error = originalConsole.error
}
export interface AcpModeOptions {
/** Path to Cline configuration directory */
config?: string
/** Working directory (default: process.cwd()) */
cwd?: string
/** Enable verbose/debug logging to stderr */
verbose?: boolean
}
/**
* Run Cline in ACP mode.
*
* This function:
* 1. Redirects console output to stderr (stdout reserved for JSON-RPC)
* 2. Sets up the ndJsonStream for stdio communication
* 3. Creates the AgentSideConnection with our AcpAgent factory
* 4. Initializes the CLI infrastructure (StateManager, Controller, etc.)
* 5. Keeps the process alive until the connection closes
*
* @param options - Configuration options for ACP mode
*/
export async function runAcpMode(options: AcpModeOptions = {}): Promise<void> {
redirectConsoleToStderr()
const outputStream = nodeToWebWritable(process.stdout)
const inputStream = nodeToWebReadable(process.stdin)
const stream = ndJsonStream(outputStream, inputStream)
let agent: AcpAgent | null = null
new AgentSideConnection((conn) => {
agent = new AcpAgent(conn, {
debug: Boolean(options.verbose),
})
return agent
}, stream)
let isShuttingDown = false
const shutdown = async () => {
if (isShuttingDown) {
// Force exit on second signal
process.exit(1)
}
isShuttingDown = true
try {
await agent?.shutdown()
restoreConsole()
} catch (error) {
Logger.error("[ACP] Error during shutdown:", error)
}
process.exit(0)
}
process.on("SIGINT", shutdown)
process.on("SIGTERM", shutdown)
// Keep the process alive
// The ndJsonStream will handle stdin events automatically.
// We need to ensure the process doesn't exit while waiting for input.
process.stdin.resume()
// Handle stdin end (client disconnected)
process.stdin.on("end", shutdown)
// Handle stdin errors
process.stdin.on("error", async (error) => {
Logger.error("[ACP] stdin error:", error)
await shutdown()
})
Logger.info("[ACP] Process is now listening for ACP requests on stdin")
}
-54
View File
@@ -1,54 +0,0 @@
/**
* Stream conversion utilities for ACP mode.
*
* The ACP SDK's ndJsonStream function expects Web Streams (ReadableStream/WritableStream),
* but Node.js provides its own stream types. These utilities convert between them.
*
* @module acp/streamUtils
*/
import type { Readable, Writable } from "node:stream"
/**
* Convert a Node.js Writable stream to a Web WritableStream.
*
* Used to convert process.stdout for ACP output.
*
* @param nodeStream - Node.js Writable stream (e.g., process.stdout)
* @returns Web WritableStream compatible with ndJsonStream
*/
export function nodeToWebWritable(nodeStream: Writable): WritableStream<Uint8Array> {
return new WritableStream<Uint8Array>({
write(chunk) {
return new Promise<void>((resolve, reject) => {
nodeStream.write(Buffer.from(chunk), (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
},
})
}
/**
* Convert a Node.js Readable stream to a Web ReadableStream.
*
* Used to convert process.stdin for ACP input.
*
* @param nodeStream - Node.js Readable stream (e.g., process.stdin)
* @returns Web ReadableStream compatible with ndJsonStream
*/
export function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Array> {
return new ReadableStream<Uint8Array>({
start(controller) {
nodeStream.on("data", (chunk: Buffer) => {
controller.enqueue(new Uint8Array(chunk))
})
nodeStream.on("end", () => controller.close())
nodeStream.on("error", (err) => controller.error(err))
},
})
}
File diff suppressed because it is too large Load Diff
-274
View File
@@ -1,274 +0,0 @@
/**
* Tests for ClineSessionEmitter - Typed EventEmitter for per-session ACP events.
*/
import { beforeEach, describe, expect, it, vi } from "vitest"
import { ClineSessionEmitter } from "./ClineSessionEmitter.js"
import type { SessionUpdatePayload } from "./types.js"
describe("ClineSessionEmitter", () => {
let emitter: ClineSessionEmitter
beforeEach(() => {
emitter = new ClineSessionEmitter()
})
describe("on/emit", () => {
it("should emit and receive agent_message_chunk events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello, world!" },
}
emitter.on("agent_message_chunk", listener)
emitter.emit("agent_message_chunk", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive agent_thought_chunk events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"agent_thought_chunk"> = {
content: { type: "text", text: "Thinking..." },
}
emitter.on("agent_thought_chunk", listener)
emitter.emit("agent_thought_chunk", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive tool_call events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"tool_call"> = {
toolCallId: "test-tool-call-id",
title: "Test Tool Call",
status: "in_progress",
}
emitter.on("tool_call", listener)
emitter.emit("tool_call", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive tool_call_update events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"tool_call_update"> = {
toolCallId: "test-tool-call-id",
status: "completed",
rawOutput: { result: "success" },
}
emitter.on("tool_call_update", listener)
emitter.emit("tool_call_update", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive available_commands_update events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"available_commands_update"> = {
availableCommands: [{ name: "test", description: "Test command" }],
}
emitter.on("available_commands_update", listener)
emitter.emit("available_commands_update", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive current_mode_update events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"current_mode_update"> = {
currentModeId: "act",
}
emitter.on("current_mode_update", listener)
emitter.emit("current_mode_update", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive plan events", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"plan"> = {
entries: [{ content: "Step 1", status: "pending", priority: "high" }],
}
emitter.on("plan", listener)
emitter.emit("plan", payload)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(payload)
})
it("should emit and receive error events", () => {
const listener = vi.fn()
const error = new Error("Test error")
emitter.on("error", listener)
emitter.emit("error", error)
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith(error)
})
})
describe("multiple listeners", () => {
it("should support multiple listeners for the same event", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.on("agent_message_chunk", listener1)
emitter.on("agent_message_chunk", listener2)
emitter.emit("agent_message_chunk", payload)
expect(listener1).toHaveBeenCalledTimes(1)
expect(listener2).toHaveBeenCalledTimes(1)
})
it("should call listeners in order of registration", () => {
const order: number[] = []
const listener1 = vi.fn(() => order.push(1))
const listener2 = vi.fn(() => order.push(2))
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.on("agent_message_chunk", listener1)
emitter.on("agent_message_chunk", listener2)
emitter.emit("agent_message_chunk", payload)
expect(order).toEqual([1, 2])
})
})
describe("off", () => {
it("should remove a specific listener", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.on("agent_message_chunk", listener)
emitter.off("agent_message_chunk", listener)
emitter.emit("agent_message_chunk", payload)
expect(listener).not.toHaveBeenCalled()
})
it("should only remove the specified listener", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.on("agent_message_chunk", listener1)
emitter.on("agent_message_chunk", listener2)
emitter.off("agent_message_chunk", listener1)
emitter.emit("agent_message_chunk", payload)
expect(listener1).not.toHaveBeenCalled()
expect(listener2).toHaveBeenCalledTimes(1)
})
})
describe("once", () => {
it("should only call the listener once", () => {
const listener = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.once("agent_message_chunk", listener)
emitter.emit("agent_message_chunk", payload)
emitter.emit("agent_message_chunk", payload)
expect(listener).toHaveBeenCalledTimes(1)
})
})
describe("removeAllListeners", () => {
it("should remove all listeners for a specific event", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
const payload: SessionUpdatePayload<"agent_message_chunk"> = {
content: { type: "text", text: "Hello" },
}
emitter.on("agent_message_chunk", listener1)
emitter.on("agent_message_chunk", listener2)
emitter.removeAllListeners("agent_message_chunk")
emitter.emit("agent_message_chunk", payload)
expect(listener1).not.toHaveBeenCalled()
expect(listener2).not.toHaveBeenCalled()
})
it("should remove all listeners when no event is specified", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
emitter.on("agent_message_chunk", listener1)
emitter.on("tool_call", listener2)
emitter.removeAllListeners()
emitter.emit("agent_message_chunk", { content: { type: "text", text: "Hello" } })
emitter.emit("tool_call", { toolCallId: "test", title: "Test" })
expect(listener1).not.toHaveBeenCalled()
expect(listener2).not.toHaveBeenCalled()
})
})
describe("listenerCount", () => {
it("should return the correct number of listeners", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
expect(emitter.listenerCount("agent_message_chunk")).toBe(0)
emitter.on("agent_message_chunk", listener1)
expect(emitter.listenerCount("agent_message_chunk")).toBe(1)
emitter.on("agent_message_chunk", listener2)
expect(emitter.listenerCount("agent_message_chunk")).toBe(2)
emitter.off("agent_message_chunk", listener1)
expect(emitter.listenerCount("agent_message_chunk")).toBe(1)
})
})
describe("chaining", () => {
it("should support method chaining", () => {
const listener = vi.fn()
const result = emitter.on("agent_message_chunk", listener).on("error", vi.fn()).off("error", vi.fn())
expect(result).toBe(emitter)
})
})
describe("emit return value", () => {
it("should return true when there are listeners", () => {
emitter.on("agent_message_chunk", vi.fn())
const result = emitter.emit("agent_message_chunk", { content: { type: "text", text: "Hello" } })
expect(result).toBe(true)
})
it("should return false when there are no listeners", () => {
const result = emitter.emit("agent_message_chunk", { content: { type: "text", text: "Hello" } })
expect(result).toBe(false)
})
})
})
-114
View File
@@ -1,114 +0,0 @@
/**
* Typed EventEmitter for per-session ACP events.
*
* This class provides a type-safe wrapper around Node's EventEmitter
* for emitting and subscribing to session-specific ACP events.
*
* @module acp
*/
import { EventEmitter } from "events"
import type { ClineSessionEvents } from "./public-types.js"
/**
* Type-safe EventEmitter for ClineAgent session events.
*
* Each session has its own emitter instance, allowing consumers to
* subscribe to events for specific sessions without filtering.
*
* @example
* ```typescript
* const agent = new ClineAgent({ version: "1.0.0" })
* const session = await agent.newSession({ cwd: "/path/to/project" })
*
* // Subscribe to session events
* agent.session(session.sessionId).on("agent_message_chunk", (content) => {
* console.log("Agent says:", content.text)
* })
*
* agent.session(session.sessionId).on("tool_call", (toolCall) => {
* console.log("Tool called:", toolCall.toolName)
* })
* ```
*/
export class ClineSessionEmitter {
private readonly emitter: EventEmitter
constructor() {
this.emitter = new EventEmitter()
// Increase max listeners since we may have many event types
this.emitter.setMaxListeners(20)
}
/**
* Subscribe to a session event.
*
* @param event - The event name to subscribe to
* @param listener - The callback function to invoke when the event is emitted
* @returns This emitter instance for chaining
*/
on<K extends keyof ClineSessionEvents>(event: K, listener: ClineSessionEvents[K]): this {
this.emitter.on(event, listener as (...args: unknown[]) => void)
return this
}
/**
* Subscribe to a session event for a single invocation.
*
* @param event - The event name to subscribe to
* @param listener - The callback function to invoke when the event is emitted
* @returns This emitter instance for chaining
*/
once<K extends keyof ClineSessionEvents>(event: K, listener: ClineSessionEvents[K]): this {
this.emitter.once(event, listener as (...args: unknown[]) => void)
return this
}
/**
* Unsubscribe from a session event.
*
* @param event - The event name to unsubscribe from
* @param listener - The callback function to remove
* @returns This emitter instance for chaining
*/
off<K extends keyof ClineSessionEvents>(event: K, listener: ClineSessionEvents[K]): this {
this.emitter.off(event, listener as (...args: unknown[]) => void)
return this
}
/**
* Emit a session event.
*
* @param event - The event name to emit
* @param args - The arguments to pass to the event listeners
* @returns True if the event had listeners, false otherwise
*/
emit<K extends keyof ClineSessionEvents>(event: K, ...args: Parameters<ClineSessionEvents[K]>): boolean {
return this.emitter.emit(event, ...args)
}
/**
* Remove all listeners for a specific event or all events.
*
* @param event - Optional event name to remove listeners for
* @returns This emitter instance for chaining
*/
removeAllListeners<K extends keyof ClineSessionEvents>(event?: K): this {
if (event) {
this.emitter.removeAllListeners(event)
} else {
this.emitter.removeAllListeners()
}
return this
}
/**
* Get the number of listeners for a specific event.
*
* @param event - The event name to count listeners for
* @returns The number of listeners
*/
listenerCount<K extends keyof ClineSessionEvents>(event: K): number {
return this.emitter.listenerCount(event)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-356
View File
@@ -1,356 +0,0 @@
/**
* Permission handling for ACP integration.
*
* This module handles the translation between ACP permission requests/responses
* and Cline's internal permission system. It maps ClineAsk types to appropriate
* ACP permission options and translates user responses back to Cline's format.
*
* @module acp/permissionHandler
*/
import type * as acp from "@agentclientprotocol/sdk"
import type { ClineAsk } from "@shared/ExtensionMessage"
import type { ClineAskResponse } from "@shared/WebviewMessage"
import { Logger } from "@/shared/services/Logger.js"
import type { AcpSessionState, ClinePermissionOption } from "./types.js"
/**
* Standard permission options for operations that support "always allow".
* Used for commands, tools, and MCP server operations.
*/
const STANDARD_PERMISSION_OPTIONS: ClinePermissionOption[] = [
{ kind: "allow_once", optionId: "allow_once", name: "Allow Once" },
{ kind: "allow_always", optionId: "allow_always", name: "Always Allow" },
{ kind: "reject_once", optionId: "reject_once", name: "Reject" },
]
/**
* Permission options for operations that don't support "always allow".
* Used for browser actions and other one-time operations.
*/
const RESTRICTED_PERMISSION_OPTIONS: ClinePermissionOption[] = [
{ kind: "allow_once", optionId: "allow_once", name: "Allow Once" },
{ kind: "reject_once", optionId: "reject_once", name: "Reject" },
]
/**
* Mapping of ClineAsk types to their permission option sets.
*/
const ASK_TYPE_PERMISSION_MAP: Partial<Record<ClineAsk, ClinePermissionOption[]>> = {
// Commands support "always allow" for auto-approval
command: STANDARD_PERMISSION_OPTIONS,
// Tool operations support "always allow"
tool: STANDARD_PERMISSION_OPTIONS,
// MCP server operations support "always allow"
use_mcp_server: STANDARD_PERMISSION_OPTIONS,
// Browser actions are one-time, no "always allow"
browser_action_launch: RESTRICTED_PERMISSION_OPTIONS,
// Command output continuation - simple allow/reject
command_output: RESTRICTED_PERMISSION_OPTIONS,
}
/**
* ClineAsk types that require permission handling.
* Other ask types (like followup, plan_mode_respond) don't need permission UI.
*/
const PERMISSION_REQUIRING_ASK_TYPES: Set<ClineAsk> = new Set([
"command",
"tool",
"browser_action_launch",
"use_mcp_server",
"command_output",
])
/**
* Result of handling a permission response.
*/
export interface PermissionHandlerResult {
/** Cline's internal response type */
response: ClineAskResponse
/** Optional text to pass with the response */
text?: string
/** Whether "always allow" was selected (for auto-approval tracking) */
alwaysAllow?: boolean
/** Whether the request was cancelled */
cancelled?: boolean
}
/**
* Check if a ClineAsk type requires permission handling.
*
* @param askType - The ClineAsk type to check
* @returns True if the ask type requires permission UI
*/
export function requiresPermission(askType: ClineAsk): boolean {
return PERMISSION_REQUIRING_ASK_TYPES.has(askType)
}
/**
* Get the appropriate permission options for a ClineAsk type.
*
* @param askType - The ClineAsk type
* @returns Array of permission options, or undefined if the ask type doesn't require permission
*/
export function getPermissionOptionsForAskType(askType: ClineAsk): acp.PermissionOption[] | undefined {
const options = ASK_TYPE_PERMISSION_MAP[askType]
if (!options) {
return undefined
}
// Convert to ACP PermissionOption format
return options.map((opt) => ({
kind: opt.kind,
optionId: opt.optionId,
name: opt.name,
}))
}
/**
* Handle an ACP permission response and translate it to Cline's format.
*
* @param response - The ACP permission response from the client
* @param askType - The original ClineAsk type that triggered the permission request
* @returns The translated result for Cline's handleWebviewAskResponse
*/
export function handlePermissionResponse(response: acp.RequestPermissionResponse, askType: ClineAsk): PermissionHandlerResult {
// Check if cancelled
if (response.outcome.outcome === "cancelled") {
return {
response: "noButtonClicked",
cancelled: true,
}
}
// Get the selected option ID
const optionId = response.outcome.optionId
// Translate the option to Cline's response format
switch (optionId) {
case "allow_once":
return {
response: "yesButtonClicked",
alwaysAllow: false,
}
case "allow_always":
return {
response: "yesButtonClicked",
alwaysAllow: true,
}
case "reject_once":
case "reject_always":
return {
response: "noButtonClicked",
alwaysAllow: false,
}
default:
// Unknown option ID - treat as rejection for safety
Logger.error(`[permissionHandler] Unknown permission option: ${optionId}`)
return {
response: "noButtonClicked",
}
}
}
/**
* Create a permission request for an ACP tool call.
*
* @param toolCall - The ACP tool call that needs permission
* @param askType - The Cline ask type
* @returns The permission request options, or null if no permission needed
*/
export function createPermissionRequest(
toolCall: acp.ToolCall,
askType: ClineAsk,
): { toolCall: acp.ToolCall; options: acp.PermissionOption[] } | null {
const options = getPermissionOptionsForAskType(askType)
if (!options) {
return null
}
return {
toolCall,
options,
}
}
/**
* Track "always allow" decisions for auto-approval.
* This maintains a set of tool/command patterns that have been auto-approved.
*/
export class AutoApprovalTracker {
/** Set of auto-approved command prefixes */
private autoApprovedCommands: Set<string> = new Set()
/** Set of auto-approved tool names */
private autoApprovedTools: Set<string> = new Set()
/** Set of auto-approved MCP servers */
private autoApprovedMcpServers: Set<string> = new Set()
/**
* Record an "always allow" decision for a permission request.
*
* @param askType - The Cline ask type that was auto-approved
* @param identifier - The identifier for the operation (command, tool name, etc.)
*/
recordAlwaysAllow(askType: ClineAsk, identifier: string): void {
switch (askType) {
case "command":
// Store the first word of the command as the key
const commandPrefix = identifier.split(" ")[0]
this.autoApprovedCommands.add(commandPrefix)
break
case "tool":
this.autoApprovedTools.add(identifier)
break
case "use_mcp_server":
this.autoApprovedMcpServers.add(identifier)
break
}
}
/**
* Check if an operation has been auto-approved.
*
* @param askType - The Cline ask type
* @param identifier - The identifier for the operation
* @returns True if the operation was previously auto-approved
*/
isAutoApproved(askType: ClineAsk, identifier: string): boolean {
switch (askType) {
case "command":
const commandPrefix = identifier.split(" ")[0]
return this.autoApprovedCommands.has(commandPrefix)
case "tool":
return this.autoApprovedTools.has(identifier)
case "use_mcp_server":
return this.autoApprovedMcpServers.has(identifier)
default:
return false
}
}
/**
* Clear all auto-approval records.
*/
clear(): void {
this.autoApprovedCommands.clear()
this.autoApprovedTools.clear()
this.autoApprovedMcpServers.clear()
}
}
/**
* Process a pending permission request for a session.
*
* This function coordinates the permission flow:
* 1. Checks if the operation is already auto-approved
* 2. If not, requests permission from the ACP client
* 3. Tracks "always allow" decisions
* 4. Returns the translated result for Cline
*
* @param requestPermission - Function to request permission from the ACP client
* @param sessionId - The session ID
* @param toolCall - The tool call requiring permission
* @param askType - The Cline ask type
* @param identifier - Identifier for auto-approval tracking
* @param autoApprovalTracker - The auto-approval tracker
* @returns The permission handler result
*/
export async function processPermissionRequest(
requestPermission: (
sessionId: string,
toolCall: acp.ToolCall,
options: acp.PermissionOption[],
) => Promise<acp.RequestPermissionResponse>,
sessionId: string,
toolCall: acp.ToolCall,
askType: ClineAsk,
identifier: string,
autoApprovalTracker?: AutoApprovalTracker,
): Promise<PermissionHandlerResult> {
// Check if already auto-approved
if (autoApprovalTracker?.isAutoApproved(askType, identifier)) {
return {
response: "yesButtonClicked",
alwaysAllow: true,
}
}
// Get permission options for this ask type
const options = getPermissionOptionsForAskType(askType)
if (!options) {
// No permission options defined - allow by default
return {
response: "yesButtonClicked",
}
}
// Request permission from the ACP client
const response = await requestPermission(sessionId, toolCall, options)
// Handle the response
const result = handlePermissionResponse(response, askType)
// Track "always allow" decisions
if (result.alwaysAllow && autoApprovalTracker) {
autoApprovalTracker.recordAlwaysAllow(askType, identifier)
}
return result
}
/**
* Get the identifier for auto-approval tracking from a tool call.
*
* @param toolCall - The ACP tool call
* @param askType - The Cline ask type
* @returns The identifier string for auto-approval tracking
*/
export function getAutoApprovalIdentifier(toolCall: acp.ToolCall, askType: ClineAsk): string {
const rawInput = toolCall.rawInput as Record<string, unknown> | undefined
switch (askType) {
case "command":
return (rawInput?.command as string) || toolCall.title
case "tool":
// Try to get tool name from raw input or title
return (rawInput?.tool as string) || toolCall.title
case "use_mcp_server":
return (rawInput?.serverName as string) || toolCall.title
default:
return toolCall.toolCallId
}
}
/**
* Update the session state's pending tool call after permission is handled.
*
* @param sessionState - The session state to update
* @param toolCallId - The tool call ID that was handled
* @param approved - Whether the permission was approved
*/
export function updateSessionStateAfterPermission(sessionState: AcpSessionState, toolCallId: string, approved: boolean): void {
// Remove from pending tool calls
sessionState.pendingToolCalls.delete(toolCallId)
// Clear current tool call ID if it matches
if (sessionState.currentToolCallId === toolCallId && !approved) {
sessionState.currentToolCallId = undefined
}
}
-254
View File
@@ -1,254 +0,0 @@
/**
* 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"
-68
View File
@@ -1,68 +0,0 @@
/**
* Internal types for ACP integration with Cline CLI.
*
* 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).
*/
export type {
Agent,
AgentSideConnection,
AudioContent,
CancelNotification,
ContentBlock,
ImageContent,
InitializeRequest,
InitializeResponse,
LoadSessionRequest,
LoadSessionResponse,
McpServer,
ModelInfo,
NewSessionRequest,
NewSessionResponse,
PermissionOption,
PermissionOptionKind,
PromptRequest,
PromptResponse,
ReadTextFileRequest,
ReadTextFileResponse,
RequestPermissionRequest,
RequestPermissionResponse,
SessionConfigOption,
SessionModelState,
SessionNotification,
SessionUpdate,
SetSessionConfigOptionRequest,
SetSessionConfigOptionResponse,
SetSessionModelRequest,
SetSessionModelResponse,
SetSessionModeRequest,
SetSessionModeResponse,
StopReason,
TextContent,
ToolCall,
ToolCallStatus,
ToolCallUpdate,
ToolKind,
WriteTextFileRequest,
WriteTextFileResponse,
} from "@agentclientprotocol/sdk"
export type {
AcpAgentOptions,
AcpSessionState,
ClineAgentCapabilities,
ClineAgentInfo,
ClineAgentOptions,
ClinePermissionOption,
ClineSessionEvents,
PermissionHandler,
SessionUpdatePayload,
SessionUpdateType,
TranslatedMessage,
} from "./public-types.js"
export { AcpSessionStatus } from "./public-types.js"
+4 -4
View File
@@ -194,7 +194,7 @@ const errorTypes = ["api_req_failed", "mistake_limit_reached"]
/**
* Get button configuration based on message type and state
*/
export function getButtonConfig(message: ClineMessage | undefined, isStreaming: boolean = false): ButtonConfig {
export function getButtonConfig(message: ClineMessage | undefined, isStreaming = false): ButtonConfig {
if (!message) {
return BUTTON_CONFIGS.default
}
@@ -295,6 +295,7 @@ export function getVisibleButtons(config: ButtonConfig) {
* Does not show cancel-only buttons (ThinkingIndicator handles that with esc)
*/
export const ActionButtons: React.FC<ActionButtonsProps> = ({ config, mode = "act" }) => {
const { columns: terminalWidth } = useTerminalSize()
if (!config.enableButtons) {
return null
}
@@ -306,7 +307,6 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({ config, mode = "ac
}
// Calculate button widths based on terminal width
const { columns: terminalWidth } = useTerminalSize()
const buttonCount = (hasPrimary ? 1 : 0) + (hasSecondary ? 1 : 0)
const gapWidth = buttonCount > 1 ? 1 : 0 // 1 char gap between buttons
const availableWidth = terminalWidth - 2 - gapWidth // 1 space padding on each side
@@ -330,8 +330,8 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({ config, mode = "ac
return (
<Box flexDirection="row" gap={1} marginLeft={1} width="100%">
{hasPrimary && renderButton(config.primaryText!, "1")}
{hasSecondary && renderButton(config.secondaryText!, hasPrimary ? "2" : "1")}
{hasPrimary && config.primaryText && renderButton(config.primaryText, "1")}
{hasSecondary && config.secondaryText && renderButton(config.secondaryText, hasPrimary ? "2" : "1")}
</Box>
)
}
+2 -2
View File
@@ -7,7 +7,7 @@ import { Box, Text, useInput } from "ink"
import React from "react"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { isMouseEscapeSequence } from "../utils/input"
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
interface ApiKeyInputProps {
providerName: string
@@ -39,7 +39,7 @@ export const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
onCancel()
return
}
if (key.return) {
if (isEnterKey(input, key)) {
onSubmit(value)
return
}
@@ -0,0 +1,136 @@
import { Text } from "ink"
import { render } from "ink-testing-library"
import React from "react"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { App } from "./App"
const CLEAR_SEQUENCE = "\x1b[2J\x1b[3J\x1b[H"
function setTerminalSize(columns: number, rows: number) {
Object.defineProperty(process.stdout, "columns", {
configurable: true,
writable: true,
value: columns,
})
Object.defineProperty(process.stdout, "rows", {
configurable: true,
writable: true,
value: rows,
})
}
function hasClearSequenceCall(calls: unknown[][]): boolean {
return calls.some((call) => call[0] === CLEAR_SEQUENCE)
}
vi.mock("./ChatView", () => ({
ChatView: ({ controller, initialPrompt, initialImages }: any) => {
React.useEffect(() => {
if (initialPrompt || (initialImages && initialImages.length > 0)) {
controller?.initTask(initialPrompt || "", initialImages)
}
}, [])
return React.createElement(Text, null, "ChatView")
},
}))
vi.mock("./TaskJsonView", () => ({
TaskJsonView: () => React.createElement(Text, null, "TaskJsonView"),
}))
vi.mock("./HistoryView", () => ({
HistoryView: () => React.createElement(Text, null, "HistoryView"),
}))
vi.mock("./ConfigView", () => ({
ConfigView: () => React.createElement(Text, null, "ConfigView"),
}))
vi.mock("./AuthView", () => ({
AuthView: () => React.createElement(Text, null, "AuthView"),
}))
vi.mock("../context/TaskContext", () => ({
TaskContextProvider: ({ children }: any) => children,
}))
vi.mock("../context/StdinContext", () => ({
StdinProvider: ({ children }: any) => children,
}))
describe("App startup prompt resize behavior", () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
delete (process.stdout as any).columns
delete (process.stdout as any).rows
})
it("does not replay initialPrompt after a width resize", async () => {
const initTask = vi.fn()
setTerminalSize(120, 40)
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((...args: any[]) => {
const callback = args.find((arg) => typeof arg === "function")
if (callback) {
callback()
}
return true
}) as any)
const { unmount } = render(
<App controller={{ initTask }} initialPrompt="hello" isRawModeSupported={true} view="welcome" />,
)
await vi.advanceTimersByTimeAsync(0)
expect(initTask).toHaveBeenCalledTimes(1)
writeSpy.mockClear()
setTerminalSize(121, 40)
process.stdout.emit("resize")
await vi.advanceTimersByTimeAsync(350)
await vi.advanceTimersByTimeAsync(0)
expect(initTask).toHaveBeenCalledTimes(1)
expect(hasClearSequenceCall(writeSpy.mock.calls as unknown[][])).toBe(true)
unmount()
})
it("does not remount on height-only resize", async () => {
const initTask = vi.fn()
setTerminalSize(120, 40)
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((...args: any[]) => {
const callback = args.find((arg) => typeof arg === "function")
if (callback) {
callback()
}
return true
}) as any)
const { unmount } = render(
<App controller={{ initTask }} initialPrompt="hello" isRawModeSupported={true} view="welcome" />,
)
await vi.advanceTimersByTimeAsync(0)
expect(initTask).toHaveBeenCalledTimes(1)
writeSpy.mockClear()
setTerminalSize(120, 45)
process.stdout.emit("resize")
await vi.advanceTimersByTimeAsync(350)
await vi.advanceTimersByTimeAsync(0)
expect(initTask).toHaveBeenCalledTimes(1)
expect(hasClearSequenceCall(writeSpy.mock.calls as unknown[][])).toBe(false)
unmount()
})
})
+27 -5
View File
@@ -3,14 +3,15 @@
* Routes between different views (task, history, config)
*/
import { Box } from "ink"
import React, { ReactNode, useCallback, useState } from "react"
import { Box, useApp } from "ink"
import React, { ReactNode, useCallback, useEffect, useState } from "react"
import { StdinProvider } from "../context/StdinContext"
import { TaskContextProvider } from "../context/TaskContext"
import { useTerminalSize } from "../hooks/useTerminalSize"
import { AuthView } from "./AuthView"
import { ChatView } from "./ChatView"
import { ConfigView } from "./ConfigView"
import { ErrorBoundary } from "./ErrorBoundary"
import { HistoryView } from "./HistoryView"
import { TaskJsonView } from "./TaskJsonView"
@@ -90,7 +91,17 @@ interface AppProps {
isRawModeSupported?: boolean
}
export const App: React.FC<AppProps> = ({
export const App: React.FC<AppProps> = (props) => {
const { exit } = useApp()
return (
<ErrorBoundary exit={exit}>
<InternalApp {...props} />
</ErrorBoundary>
)
}
const InternalApp: React.FC<AppProps> = ({
view: initialView,
taskId,
verbose = false,
@@ -135,6 +146,17 @@ export const App: React.FC<AppProps> = ({
const { resizeKey } = useTerminalSize()
const [currentView, setCurrentView] = useState<ViewType>(initialView)
const [selectedTaskId, setSelectedTaskId] = useState<string | undefined>(taskId)
const [pendingInitialPrompt, setPendingInitialPrompt] = useState<string | undefined>(initialPrompt)
const [pendingInitialImages, setPendingInitialImages] = useState<string[] | undefined>(initialImages)
useEffect(() => {
if (!pendingInitialPrompt && (!pendingInitialImages || pendingInitialImages.length === 0)) {
return
}
setPendingInitialPrompt(undefined)
setPendingInitialImages(undefined)
}, [pendingInitialPrompt, pendingInitialImages])
const handleSelectTask = useCallback((taskId: string) => {
setSelectedTaskId(taskId)
@@ -242,8 +264,8 @@ export const App: React.FC<AppProps> = ({
) : (
<ChatView
controller={controller}
initialImages={initialImages}
initialPrompt={initialPrompt}
initialImages={pendingInitialImages}
initialPrompt={pendingInitialPrompt}
onComplete={onComplete}
onError={onError}
onExit={onWelcomeExit}
+43 -44
View File
@@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react"
import { useStdinContext } from "../context/StdinContext"
import { useTaskController } from "../context/TaskContext"
import { useLastCompletedAskMessage } from "../hooks/useStateSubscriber"
import { isMouseEscapeSequence } from "../utils/input"
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
import { jsonParseSafe } from "../utils/parser"
interface AskPromptProps {
@@ -136,7 +136,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
} else if (promptType === "options") {
// Number selection for options, or free text input
const parts = jsonParseSafe(text, { options: [] as string[] })
if (key.return) {
if (isEnterKey(input, key)) {
// Submit free text on Enter
if (textInput.trim()) {
sendResponse("messageResponse", textInput.trim())
@@ -145,7 +145,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
setTextInput((prev) => prev.slice(0, -1))
} else if (input && !key.ctrl && !key.meta) {
// Check if it's a number for option selection (only when no text typed yet)
const num = parseInt(input, 10)
const num = Number.parseInt(input, 10)
if (textInput === "" && !Number.isNaN(num) && num >= 1 && num <= parts.options.length) {
const selectedOption = parts.options[num - 1]
sendResponse("messageResponse", selectedOption)
@@ -156,7 +156,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
}
} else if (promptType === "text") {
// Text input mode
if (key.return) {
if (isEnterKey(input, key)) {
// Submit on Enter
if (textInput.trim()) {
sendResponse("messageResponse", textInput.trim())
@@ -169,7 +169,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
}
} else if (promptType === "plan_mode_text") {
// Plan mode text input - allows text response or toggle to Act mode
if (key.return) {
if (isEnterKey(input, key)) {
// Submit on Enter
if (textInput.trim()) {
sendResponse("messageResponse", textInput.trim())
@@ -185,7 +185,7 @@ export const AskPrompt: React.FC<AskPromptProps> = ({ onRespond }) => {
}
} else if (promptType === "completion") {
// Task completed - allow follow-up question or exit
if (key.return) {
if (isEnterKey(input, key)) {
if (textInput.trim()) {
// Send follow-up question
sendResponse("messageResponse", textInput.trim())
@@ -401,43 +401,42 @@ function getCliMessagePrefixIcon(message: ClineMessage): string {
default:
return "❔"
}
} else {
switch (message.say) {
case "task":
return "📋"
case "error":
return "❌"
case "text":
return "💬"
case "reasoning":
return "🧠"
case "completion_result":
return "✅"
case "user_feedback":
return "👤"
case "command":
case "command_output":
return "⚙️"
case "tool":
return "🔧"
case "browser_action":
case "browser_action_launch":
case "browser_action_result":
return "🌐"
case "mcp_server_request_started":
case "mcp_server_response":
return "🔌"
case "api_req_started":
case "api_req_finished":
return "🔄"
case "checkpoint_created":
return "💾"
case "info":
return "️"
case "generate_explanation":
return "📝"
default:
return " "
}
}
switch (message.say) {
case "task":
return "📋"
case "error":
return "❌"
case "text":
return "💬"
case "reasoning":
return "🧠"
case "completion_result":
return "✅"
case "user_feedback":
return "👤"
case "command":
case "command_output":
return "⚙️"
case "tool":
return "🔧"
case "browser_action":
case "browser_action_launch":
case "browser_action_result":
return "🌐"
case "mcp_server_request_started":
case "mcp_server_response":
return "🔌"
case "api_req_started":
case "api_req_finished":
return "🔄"
case "checkpoint_created":
return "💾"
case "info":
return "️"
case "generate_explanation":
return "📝"
default:
return " "
}
}
+7 -7
View File
@@ -19,7 +19,7 @@ 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"
import { isMouseEscapeSequence } from "../utils/input"
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
import { applyBedrockConfig, applyProviderConfig } from "../utils/provider-config"
import { useValidProviders } from "../utils/providers"
import { ApiKeyInput } from "./ApiKeyInput"
@@ -79,12 +79,12 @@ const Select: React.FC<{
const [selectedIndex, setSelectedIndex] = useState(0)
useInput(
(_, key) => {
(input, key) => {
if (key.upArrow) {
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1))
} else if (key.downArrow) {
setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0))
} else if (key.return) {
} else if (isEnterKey(input, key)) {
onSelect(items[selectedIndex].value)
}
},
@@ -130,7 +130,7 @@ const TextInput: React.FC<{
return
}
if (key.return) {
if (isEnterKey(input, key)) {
onSubmit(value)
} else if (key.backspace || key.delete) {
onChange(value.slice(0, -1))
@@ -853,7 +853,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setMenuIndex((prev) => (prev > 0 ? prev - 1 : mainMenuItems.length - 1))
} else if (key.downArrow) {
setMenuIndex((prev) => (prev < mainMenuItems.length - 1 ? prev + 1 : 0))
} else if (key.return) {
} else if (isEnterKey(input, key)) {
handleMainMenuSelect(mainMenuItems[menuIndex].value)
}
} else if (step === "provider") {
@@ -861,7 +861,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setProviderIndex((prev) => (prev > 0 ? prev - 1 : providerItems.length - 1))
} else if (key.downArrow) {
setProviderIndex((prev) => (prev < providerItems.length - 1 ? prev + 1 : 0))
} else if (key.return) {
} else if (isEnterKey(input, key)) {
if (providerItems[providerIndex]) {
handleProviderSelect(providerItems[providerIndex].value)
}
@@ -877,7 +877,7 @@ export const AuthView: React.FC<AuthViewProps> = ({ controller, onComplete, onEr
setClineModelIndex((prev) => (prev > 0 ? prev - 1 : maxIndex))
} else if (key.downArrow) {
setClineModelIndex((prev) => (prev < maxIndex ? prev + 1 : 0))
} else if (key.return) {
} else if (isEnterKey(input, key)) {
if (isBrowseAllSelected(clineModelIndex, featuredModels)) {
setStep("modelid")
} else {
@@ -9,6 +9,7 @@ import { Box, Text, useInput } from "ink"
import React, { useCallback, useState } from "react"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { isEnterKey } from "../utils/input"
import { getModelList } from "./ModelPicker"
import { SearchableList } from "./SearchableList"
@@ -43,7 +44,7 @@ export const BedrockCustomModelFlow: React.FC<BedrockCustomModelFlowProps> = ({
if (step === "arn_input") {
if (key.escape) {
onCancel()
} else if (key.return) {
} else if (isEnterKey(input, key)) {
handleArnSubmit()
} else if (key.backspace || key.delete) {
setCustomArn((prev) => prev.slice(0, -1))
+11 -7
View File
@@ -12,7 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { ChatView } from "./ChatView"
// Helper to wait for async state updates
const delay = (ms: number = 60) => new Promise((resolve) => setTimeout(resolve, ms))
const delay = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms))
// Type for our exit mock function
type ExitMockFn = ReturnType<typeof vi.fn> & (() => void)
@@ -126,12 +126,16 @@ vi.mock("../utils/file-search", () => ({
searchWorkspaceFiles: vi.fn(async () => []),
}))
vi.mock("../utils/slash-commands", () => ({
extractSlashQuery: vi.fn(() => ({ inSlashMode: false, query: "", slashIndex: -1 })),
filterCommands: vi.fn(() => []),
insertSlashCommand: vi.fn((text: string) => text),
sortCommandsWorkflowsFirst: vi.fn((cmds: unknown[]) => cmds),
}))
vi.mock("../utils/slash-commands", async (importOriginal) => {
const actual = await importOriginal<typeof import("../utils/slash-commands")>()
return {
...actual,
extractSlashQuery: vi.fn(() => ({ inSlashMode: false, query: "", slashIndex: -1 })),
filterCommands: vi.fn(() => []),
insertSlashCommand: vi.fn((text: string) => text),
sortCommandsWorkflowsFirst: vi.fn((cmds: unknown[]) => cmds),
}
})
vi.mock("../utils/input", () => ({
isMouseEscapeSequence: vi.fn(() => false),
+100 -71
View File
@@ -108,7 +108,6 @@ import type { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
import { getApiMetrics, getLastApiReqTotalTokens } from "@shared/getApiMetrics"
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
import type { SlashCommandInfo } from "@shared/proto/cline/slash"
import { CLI_ONLY_COMMANDS } from "@shared/slashCommands"
import { getProviderDefaultModelId, getProviderModelIdKey } from "@shared/storage"
import type { Mode } from "@shared/storage/types"
import { execSync } from "child_process"
@@ -137,7 +136,14 @@ import {
} from "../utils/file-search"
import { isMouseEscapeSequence } from "../utils/input"
import { jsonParseSafe, parseImagesFromInput } from "../utils/parser"
import { extractSlashQuery, filterCommands, insertSlashCommand, sortCommandsWorkflowsFirst } from "../utils/slash-commands"
import {
createCliOnlySlashCommands,
extractSlashQuery,
filterCommands,
getStandaloneSlashCommandToExecute,
insertSlashCommand,
sortCommandsWorkflowsFirst,
} from "../utils/slash-commands"
import { waitFor } from "../utils/timeout"
import { isFileEditTool, parseToolFromMessage } from "../utils/tools"
import { shutdownEvent } from "../vscode-shim"
@@ -403,7 +409,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
const PASTE_UPDATE_DEBOUNCE_MS = 50 // Debounce visual updates to avoid flicker
// Slash command state
const [availableCommands, setAvailableCommands] = useState<SlashCommandInfo[]>([])
const [availableCommands, setAvailableCommands] = useState<SlashCommandInfo[]>(() => createCliOnlySlashCommands())
const [selectedSlashIndex, setSelectedSlashIndex] = useState(0)
const [slashMenuDismissed, setSlashMenuDismissed] = useState(false)
const lastSlashIndexRef = useRef<number>(-1)
@@ -614,16 +620,15 @@ export const ChatView: React.FC<ChatViewProps> = ({
try {
const response = await getAvailableSlashCommands(ctrl, EmptyRequest.create())
const cliCommands = response.commands.filter((cmd) => cmd.cliCompatible !== false)
// Add CLI-only commands (like /settings) that are handled locally
const cliOnlyCommands: SlashCommandInfo[] = CLI_ONLY_COMMANDS.map((cmd) => ({
name: cmd.name,
description: cmd.description || "",
section: cmd.section || "default",
cliCompatible: true,
}))
// Add CLI-only commands (like /settings) that are handled locally.
// Seed these synchronously on first render so locally handled commands like
// /q and /exit are immediately available, even before the async command
// fetch completes. This avoids a race that can make the quit command tests
// flaky on slower Windows CI runners.
const cliOnlyCommands = createCliOnlySlashCommands()
setAvailableCommands([...cliOnlyCommands, ...sortCommandsWorkflowsFirst(cliCommands)])
} catch {
// Fallback: commands will be empty, menu won't show
// Keep CLI-only commands available even if backend command loading fails.
}
}
loadCommands()
@@ -843,6 +848,77 @@ export const ChatView: React.FC<ChatViewProps> = ({
}, 150)
}, [inkExit, onExit])
const handleCliOnlySlashCommand = useCallback(
(commandName: string): boolean => {
if (commandName === "help") {
setActivePanel({ type: "help" })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return true
}
if (commandName === "settings") {
setActivePanel({ type: "settings" })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return true
}
if (commandName === "models") {
const apiConfig = StateManager.get().getApiConfiguration()
const provider =
mode === "act"
? apiConfig.actModeApiProvider || apiConfig.planModeApiProvider
: apiConfig.planModeApiProvider || apiConfig.actModeApiProvider
const initialMode = !provider ? undefined : provider === "cline" ? "featured-models" : "model-picker"
const initialModelKey = mode === "act" ? "actModelId" : "planModelId"
setActivePanel({ type: "settings", initialMode, initialModelKey })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return true
}
if (commandName === "history") {
setActivePanel({ type: "history" })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return true
}
if (commandName === "skills") {
setActivePanel({ type: "skills" })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return true
}
if (commandName === "clear") {
void clearViewAndResetTask()
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return true
}
if (commandName === "exit" || commandName === "q") {
handleExit()
return true
}
return false
},
[clearViewAndResetTask, handleExit, mode, setCursorPos, setTextInput],
)
// Get button config based on the last message state
const buttonConfig = useMemo(() => {
const lastMsg = messages[messages.length - 1] as ClineMessage | undefined
@@ -1102,6 +1178,17 @@ export const ChatView: React.FC<ChatViewProps> = ({
const inSlashMenu = slashInfo.inSlashMode && filteredCommands.length > 0 && !slashMenuDismissed
const inFileMenu = mentionInfo.inMentionMode && fileResults.length > 0 && !inSlashMenu
const standaloneSlashCommand = getStandaloneSlashCommandToExecute({
prompt,
inSlashMode: slashInfo.inSlashMode,
hasSlashMenu: inSlashMenu,
hasPendingAsk: !!pendingAsk,
isSpinnerActive,
})
if (key.return && standaloneSlashCommand && handleCliOnlySlashCommand(standaloneSlashCommand)) {
return
}
// 5. Slash command menu navigation (takes priority over file menu)
if (inSlashMenu) {
@@ -1116,64 +1203,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
if (key.tab || key.return) {
const cmd = filteredCommands[selectedSlashIndex]
if (cmd) {
// Handle CLI-only commands locally
if (cmd.name === "help") {
setActivePanel({ type: "help" })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return
}
if (cmd.name === "settings") {
setActivePanel({ type: "settings" })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return
}
if (cmd.name === "models") {
const apiConfig = StateManager.get().getApiConfiguration()
// Use current mode's provider to determine picker type
const provider =
mode === "act"
? apiConfig.actModeApiProvider || apiConfig.planModeApiProvider
: apiConfig.planModeApiProvider || apiConfig.actModeApiProvider
const initialMode = !provider ? undefined : provider === "cline" ? "featured-models" : "model-picker"
// Set model for current mode (plan or act)
const initialModelKey = mode === "act" ? "actModelId" : "planModelId"
setActivePanel({ type: "settings", initialMode, initialModelKey })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
setSlashMenuDismissed(true)
return
}
if (cmd.name === "history") {
setActivePanel({ type: "history" })
setTextInput("")
setCursorPos(0)
setSelectedSlashIndex(0)
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" || cmd.name === "q") {
handleExit()
if (handleCliOnlySlashCommand(cmd.name)) {
return
}
const newText = insertSlashCommand(textInput, slashInfo.slashIndex, cmd.name)
@@ -1462,13 +1492,12 @@ export const ChatView: React.FC<ChatViewProps> = ({
if (item.type === "header") {
// Show static robot frame in header (first frame, looking straight ahead)
return (
<Box flexDirection="column" key="header">
<Box flexDirection="column" key="header" marginBottom={1}>
<StaticRobotFrame />
<Text> </Text>
<Text bold color="white">
{centerText("What can I do for you?")}
</Text>
<Text> </Text>
</Box>
)
}
+4 -3
View File
@@ -7,6 +7,7 @@ import type { ClineMessage } from "@shared/ExtensionMessage"
import { Box, Text, useInput } from "ink"
import React, { useState } from "react"
import { useStdinContext } from "../context/StdinContext"
import { isEnterKey } from "../utils/input"
export type RestoreType = "task" | "workspace" | "taskAndWorkspace"
@@ -101,7 +102,7 @@ export const CheckpointMenu: React.FC<CheckpointMenuProps> = ({ messages, onSele
setSelectedCheckpoint((i) => Math.max(0, i - 1))
} else if (key.downArrow) {
setSelectedCheckpoint((i) => Math.min(checkpoints.length - 1, i + 1))
} else if (key.return && checkpoints.length > 0) {
} else if (isEnterKey(input, key) && checkpoints.length > 0) {
setStage("restoreType")
}
} else if (stage === "restoreType") {
@@ -109,7 +110,7 @@ export const CheckpointMenu: React.FC<CheckpointMenuProps> = ({ messages, onSele
setSelectedRestoreType((i) => Math.max(0, i - 1))
} else if (key.downArrow) {
setSelectedRestoreType((i) => Math.min(RESTORE_TYPE_OPTIONS.length - 1, i + 1))
} else if (key.return) {
} else if (isEnterKey(input, key)) {
const checkpoint = checkpoints[selectedCheckpoint]
const restoreType = RESTORE_TYPE_OPTIONS[selectedRestoreType]
if (checkpoint && restoreType) {
@@ -120,7 +121,7 @@ export const CheckpointMenu: React.FC<CheckpointMenuProps> = ({ messages, onSele
// Quick number selection for checkpoints
if (stage === "checkpoint") {
const num = parseInt(input, 10)
const num = Number.parseInt(input, 10)
if (!Number.isNaN(num) && num >= 1 && num <= checkpoints.length) {
setSelectedCheckpoint(num - 1)
setStage("restoreType")
+10
View File
@@ -84,6 +84,16 @@ describe("ConfigView", () => {
)
expect(lastFrame()).toContain("Global Settings")
})
it("hides Hooks tab when hooks are disabled", () => {
const { lastFrame } = render(<ConfigView {...defaultProps} hooksEnabled={false} skillsEnabled={true} />)
expect(lastFrame()).not.toContain("Hooks")
})
it("shows Hooks tab when hooks are enabled", () => {
const { lastFrame } = render(<ConfigView {...defaultProps} hooksEnabled={true} skillsEnabled={true} />)
expect(lastFrame()).toContain("Hooks")
})
})
describe("value formatting", () => {
+7 -1
View File
@@ -56,7 +56,13 @@ export interface ObjectEditorState {
editValue: string
}
export const EXCLUDED_KEYS = new Set(["taskHistory", "primaryRootIndex", "welcomeViewCompleted", "isNewUser"])
export const EXCLUDED_KEYS = new Set([
"taskHistory",
"primaryRootIndex",
"welcomeViewCompleted",
"isNewUser",
"cliKanbanMigrationAnnouncementShown",
])
export const EDITABLE_TYPES: Set<ValueType> = new Set(["string", "number", "boolean", "object"])
export const MAX_VISIBLE = 12
+51
View File
@@ -0,0 +1,51 @@
import { Box, Text } from "ink"
import React from "react"
import { ErrorService } from "@/services/error"
import { StaticRobotFrame } from "./AsciiMotionCli"
type Props = React.PropsWithChildren<{ exit: (error?: Error) => void }>
async function onReactError(props: Props, error: Error, errorInfo: React.ErrorInfo) {
try {
await ErrorService.get().captureException(error, { context: "ErrorBoundary", errorInfo })
await ErrorService.get().dispose()
} catch {
// Ignore errors
} finally {
props.exit(error)
}
}
export class ErrorBoundary extends React.Component<Props, { hasError: boolean }> {
override state = { hasError: false }
constructor(props: Props) {
super(props)
}
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
onReactError(this.props, error, errorInfo)
}
static getDerivedStateFromError() {
return { hasError: true }
}
override render() {
if (this.state.hasError) {
return (
<Box flexDirection="column" height="100%" key="header" width="100%">
<StaticRobotFrame />
<Text> </Text>
<Text bold color="white">
Something went wrong. We're sorry.
</Text>
<Text color="white">Please check the logs for more details.</Text>
<Text> </Text>
</Box>
)
}
return this.props.children
}
}
+117
View File
@@ -0,0 +1,117 @@
/**
* Rotating feature tips shown during thinking/acting phases.
* Appears after a brief delay and cycles through tips to educate users
* about Cline features while they wait.
*/
import { Box, Text } from "ink"
import React, { useCallback, useEffect, useRef, useState } from "react"
interface FeatureTipItem {
text: string
}
const FEATURE_TIPS: FeatureTipItem[] = [
{
text: 'Enable "Double-Check Completion" in settings to have Cline verify its work before finishing a task.',
},
{
text: "Add a .clinerules file to your project root to give Cline project-specific instructions.",
},
{
text: "Press Tab to switch between Plan and Act mode — plan an approach before Cline takes action.",
},
{
text: "Use @ in the chat input to add files, folders, or URLs as context for your task.",
},
{
text: "Set up MCP Servers to give Cline access to external tools and APIs.",
},
{
text: "Cline creates checkpoints after changes — you can always restore to a previous state.",
},
{
text: "Use /compact to condense long conversations and free up context window space.",
},
{
text: "Enable auto-approve for read-only tools like file reads to speed up exploration.",
},
{
text: "Use /settings to configure your API provider and model without leaving the terminal.",
},
{
text: "You can pass images with --images flag or paste image file paths in the chat.",
},
{
text: "Cline can browse websites — ask it to test your local dev server in the browser.",
},
{
text: "Use /reportbug to quickly file a GitHub issue with diagnostic context included.",
},
{
text: "Try 'npm i -g cline' to manage tasks on a Kankan board — orchestrate coding agents across worktrees.",
},
{
text: "Use Shift+Tab to toggle auto-approve all — let Cline work uninterrupted on trusted tasks.",
},
{
text: "Press Up/Down arrows in an empty input to browse your previous task prompts.",
},
{
text: "Type / to see all available commands — /history, /compact, /settings, and more.",
},
{
text: "Use /skills to browse and attach reusable skill files that guide Cline's behavior.",
},
{
text: 'You can disable these tips in /settings → Features → "Feature tips".',
},
]
const SHOW_DELAY_MS = 2000
const CYCLE_INTERVAL_MS = 8000
/**
* Shows rotating feature tips below the thinking indicator.
* Appears after a brief delay and cycles through tips while Cline is thinking/acting.
*/
export const FeatureTip: React.FC = React.memo(() => {
const [isVisible, setIsVisible] = useState(false)
const [tipIndex, setTipIndex] = useState(Math.floor(Math.random() * FEATURE_TIPS.length))
const cycleTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
const showTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const currentTip = FEATURE_TIPS[tipIndex]
const advanceTip = useCallback(() => {
setTipIndex((prev) => (prev + 1) % FEATURE_TIPS.length)
}, [])
useEffect(() => {
showTimerRef.current = setTimeout(() => {
setIsVisible(true)
cycleTimerRef.current = setInterval(advanceTip, CYCLE_INTERVAL_MS)
}, SHOW_DELAY_MS)
return () => {
if (showTimerRef.current) {
clearTimeout(showTimerRef.current)
}
if (cycleTimerRef.current) {
clearInterval(cycleTimerRef.current)
}
}
}, [advanceTip])
if (!isVisible) {
return null
}
return (
<Box paddingLeft={1}>
<Text color="gray">
💡 <Text bold>Tip:</Text> {currentTip.text}
</Text>
</Box>
)
})
+2 -2
View File
@@ -13,7 +13,7 @@ import { showTaskWithId } from "@/core/controller/task/showTaskWithId"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { useTerminalSize } from "../hooks/useTerminalSize"
import { isMouseEscapeSequence } from "../utils/input"
import { isEnterKey, isMouseEscapeSequence } from "../utils/input"
import { Panel } from "./Panel"
interface TaskHistoryItem {
@@ -142,7 +142,7 @@ export const HistoryPanelContent: React.FC<HistoryPanelContentProps> = ({ onClos
return
}
if (key.return && items[selectedIndex]) {
if (isEnterKey(input, key) && items[selectedIndex]) {
handleSelect(items[selectedIndex])
return
}
+3 -2
View File
@@ -10,6 +10,7 @@ import { showTaskWithId } from "@/core/controller/task/showTaskWithId"
import { StringRequest } from "@/shared/proto/cline/common"
import { useStdinContext } from "../context/StdinContext"
import { useTerminalSize } from "../hooks/useTerminalSize"
import { isEnterKey } from "../utils/input"
interface TaskHistoryItem {
id: string
@@ -40,7 +41,7 @@ interface HistoryViewProps {
/**
* Format separator
*/
function formatSeparator(char: string = "─", width: number = 80): string {
function formatSeparator(char = "─", width = 80): string {
return char.repeat(Math.max(width, 10))
}
@@ -111,7 +112,7 @@ export const HistoryView: React.FC<HistoryViewProps> = ({
setSelectedIndex((prev) => Math.max(0, prev - 1))
} else if (key.downArrow || input === "j") {
setSelectedIndex((prev) => Math.min(pageItems.length - 1, prev + 1))
} else if (key.return && pageItems[selectedIndex]) {
} else if (isEnterKey(input, key) && pageItems[selectedIndex]) {
onSelect(pageItems[selectedIndex])
} else if (key.leftArrow && hasPrevPage) {
handlePageChange(currentPage - 1)
+4 -3
View File
@@ -16,6 +16,7 @@ import {
importFromCodex,
importFromOpenCode,
} from "../utils/import-configs"
import { isEnterKey } from "../utils/input"
import { applyProviderConfig } from "../utils/provider-config"
type ImportStep = "select" | "confirm" | "saving" | "error"
@@ -95,13 +96,13 @@ export const ImportView: React.FC<ImportViewProps> = ({ source, onComplete, onCa
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : keys.length - 1))
} else if (key.downArrow) {
setSelectedIndex((prev) => (prev < keys.length - 1 ? prev + 1 : 0))
} else if (key.return) {
} else if (isEnterKey(input, key)) {
setStep("confirm")
}
} else if (step === "confirm") {
if (key.upArrow || key.downArrow) {
setConfirmIndex((prev) => (prev === 0 ? 1 : 0))
} else if (key.return) {
} else if (isEnterKey(input, key)) {
if (confirmIndex === 0) {
handleConfirm()
} else {
@@ -109,7 +110,7 @@ export const ImportView: React.FC<ImportViewProps> = ({ source, onComplete, onCa
}
}
} else if (step === "error") {
if (key.return) {
if (isEnterKey(input, key)) {
onCancel()
}
}
@@ -0,0 +1,27 @@
import { render } from "ink-testing-library"
import { createElement } from "react"
import { describe, expect, it, vi } from "vitest"
import { KanbanMigrationView } from "./KanbanMigrationView"
describe("KanbanMigrationView", () => {
it("renders the migration options", () => {
const onSelect = vi.fn()
const { lastFrame } = render(createElement(KanbanMigrationView, { isRawModeSupported: true, onSelect }))
expect(lastFrame()).toContain("Introducing Cline Kanban!")
expect(lastFrame()).toContain("Open the new experience")
expect(lastFrame()).toContain("Launch Cline Kanban and start there by default.")
expect(lastFrame()).toContain("cline --tui")
expect(lastFrame()).toContain("You can always run cline --tui for the terminal experience.")
expect(lastFrame()).toContain("Exit")
})
it("selects the highlighted option with Enter", () => {
const onSelect = vi.fn()
const { stdin } = render(createElement(KanbanMigrationView, { isRawModeSupported: true, onSelect }))
stdin.write("\r")
expect(onSelect).toHaveBeenCalledWith("kanban")
})
})
@@ -0,0 +1,95 @@
import { Box, Text, useApp, useInput } from "ink"
import React, { useMemo, useState } from "react"
import { COLORS } from "../constants/colors"
import { StdinProvider, useStdinContext } from "../context/StdinContext"
import { isEnterKey } from "../utils/input"
import { type KanbanMigrationAction } from "../utils/kanban"
import { StaticRobotFrame } from "./AsciiMotionCli"
import { ErrorBoundary } from "./ErrorBoundary"
interface KanbanMigrationViewProps {
isRawModeSupported: boolean
onSelect: (action: KanbanMigrationAction) => void
}
interface MigrationMenuItem {
label: string
description: string
value: KanbanMigrationAction
}
const InternalKanbanMigrationView: React.FC<Pick<KanbanMigrationViewProps, "onSelect">> = ({ onSelect }) => {
const { exit } = useApp()
const { isRawModeSupported } = useStdinContext()
const items = useMemo<MigrationMenuItem[]>(
() => [
{
label: "Open the new experience",
description: "Launch Cline Kanban and start there by default.",
value: "kanban",
},
{
label: "Exit",
description: "You can always run cline --tui for the terminal experience.",
value: "exit",
},
],
[],
)
const [selectedIndex, setSelectedIndex] = useState(0)
useInput(
(input, key) => {
if (key.escape) {
onSelect("exit")
exit()
} else if (key.upArrow) {
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1))
} else if (key.downArrow) {
setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0))
} else if (isEnterKey(input, key)) {
onSelect(items[selectedIndex].value)
exit()
}
},
{ isActive: isRawModeSupported },
)
return (
<Box flexDirection="column" width="100%">
<StaticRobotFrame />
<Text> </Text>
<Text bold color="white">
Introducing Cline Kanban!
</Text>
<Text color="gray">A board for orchestrating coding agents across worktrees, right from your browser.</Text>
<Text> </Text>
{items.map((item, index) => {
const isSelected = index === selectedIndex
return (
<Box flexDirection="column" key={item.value} marginBottom={1}>
<Text color={isSelected ? COLORS.primaryBlue : undefined}>
{isSelected ? " " : " "}
{item.label}
</Text>
<Text color="gray"> {item.description}</Text>
</Box>
)
})}
<Text> </Text>
<Text color="gray">Use arrow keys to navigate, Enter to select, Esc or Ctrl+C to exit</Text>
</Box>
)
}
export const KanbanMigrationView: React.FC<KanbanMigrationViewProps> = ({ isRawModeSupported, onSelect }) => {
const { exit } = useApp()
return (
<ErrorBoundary exit={exit}>
<StdinProvider isRawModeSupported={isRawModeSupported}>
<InternalKanbanMigrationView onSelect={onSelect} />
</StdinProvider>
</ErrorBoundary>
)
}
+3
View File
@@ -62,6 +62,8 @@ import {
sapAiCoreModels,
vertexDefaultModelId,
vertexModels,
wandbDefaultModelId,
wandbModels,
xaiDefaultModelId,
xaiModels,
} from "@/shared/api"
@@ -101,6 +103,7 @@ export const providerModels: Record<string, { models: Record<string, unknown>; d
sambanova: { models: sambanovaModels, defaultId: sambanovaDefaultModelId },
sapaicore: { models: sapAiCoreModels, defaultId: sapAiCoreDefaultModelId },
vertex: { models: vertexModels, defaultId: vertexDefaultModelId },
wandb: { models: wandbModels, defaultId: wandbDefaultModelId },
xai: { models: xaiModels, defaultId: xaiDefaultModelId },
zai: { models: internationalZAiModels, defaultId: internationalZAiDefaultModelId },
}
+26 -100
View File
@@ -1,112 +1,38 @@
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"
import { CLI_ONLY_COMMANDS } from "@shared/slashCommands"
import { describe, expect, it } from "vitest"
import { filterCommands, getStandaloneSlashCommandName, getStandaloneSlashCommandToExecute } from "../utils/slash-commands"
// 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(),
const cliOnlySlashCommands = CLI_ONLY_COMMANDS.map((cmd) => ({
name: cmd.name,
description: cmd.description || "",
section: cmd.section || "default",
cliCompatible: true,
}))
// 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()
it("prioritizes /q as the selected slash command for an exact q query", () => {
const result = filterCommands(cliOnlySlashCommands, "q")
beforeEach(() => {
vi.clearAllMocks()
expect(result[0]?.name).toBe("q")
})
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("detects /q as a standalone slash command", () => {
expect(getStandaloneSlashCommandName("/q")).toBe("q")
})
it("should exit the application when /exit is selected from slash menu", async () => {
const { stdin } = render(<ChatView onExit={mockOnExit} />)
await delay()
it("detects /exit as a standalone slash command", () => {
expect(getStandaloneSlashCommandName("/exit")).toBe("exit")
})
// 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()
it("resolves /q to direct execution when no slash menu is active", () => {
expect(
getStandaloneSlashCommandToExecute({
prompt: "/q",
inSlashMode: true,
hasSlashMenu: false,
hasPendingAsk: false,
isSpinnerActive: false,
}),
).toBe("q")
})
})
+2 -1
View File
@@ -8,6 +8,7 @@ import { Box, Text, useInput } from "ink"
import React, { useState } from "react"
import { COLORS } from "../constants/colors"
import { useStdinContext } from "../context/StdinContext"
import { isEnterKey } from "../utils/input"
export interface SelectListItem {
id: string
@@ -31,7 +32,7 @@ export function SelectList<T extends SelectListItem>({ items, onSelect, isActive
setSelectedIndex((i) => (i > 0 ? i - 1 : items.length - 1))
} else if (key.downArrow) {
setSelectedIndex((i) => (i < items.length - 1 ? i + 1 : 0))
} else if (key.return) {
} else if (isEnterKey(_input, key)) {
const item = items[selectedIndex]
if (item) {
onSelect(item)
+6 -6
View File
@@ -105,12 +105,6 @@ const FEATURE_SETTINGS = {
label: "Web tools",
description: "Enable web search and fetch tools",
},
strictPlanMode: {
stateKey: "strictPlanModeEnabled",
default: true,
label: "Strict plan mode",
description: "Require explicit mode switching",
},
nativeToolCall: {
stateKey: "nativeToolCallEnabled",
default: true,
@@ -129,6 +123,12 @@ const FEATURE_SETTINGS = {
label: "Double-check completion",
description: "Reject first completion attempt and require re-verification",
},
showFeatureTips: {
stateKey: "showFeatureTips",
default: true,
label: "Feature tips",
description: "Show tips during thinking phases",
},
} as const
type FeatureKey = keyof typeof FEATURE_SETTINGS
+74 -30
View File
@@ -38,6 +38,46 @@ import { SkillsPanelContent } from "./SkillsPanelContent"
// Helper to wait for async state updates
const delay = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms))
type WaitForConditionOptions = {
timeoutMs?: number
intervalMs?: number
errorMessage: string
}
const waitForCondition = async (
condition: () => boolean,
{ timeoutMs = 1000, intervalMs = 25, errorMessage }: WaitForConditionOptions,
) => {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
if (condition()) {
return
}
await delay(intervalMs)
}
throw new Error(errorMessage)
}
const waitForFrameToInclude = async (lastFrame: () => string | undefined, text: string) =>
waitForCondition(() => (lastFrame() || "").includes(text), {
errorMessage: `Expected frame to include: ${text}`,
})
const waitForFrameToExclude = async (lastFrame: () => string | undefined, text: string) =>
waitForCondition(() => !(lastFrame() || "").includes(text), {
errorMessage: `Expected frame to exclude: ${text}`,
})
const waitForMockToBeCalled = async (mockFn: { mock: { calls: unknown[] } }) =>
waitForCondition(() => mockFn.mock.calls.length > 0, {
errorMessage: "Expected mock to be called",
})
const waitForSkillsPanelReady = async (lastFrame: () => string | undefined, expectedText: string) => {
await waitForFrameToExclude(lastFrame, "Loading skills...")
await waitForFrameToInclude(lastFrame, expectedText)
}
describe("SkillsPanelContent", () => {
const mockController = {} as any
const mockOnClose = vi.fn()
@@ -64,11 +104,11 @@ describe("SkillsPanelContent", () => {
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
const { stdin, lastFrame } = render(<SkillsPanelContent {...defaultProps} />)
await waitForSkillsPanelReady(lastFrame, "No skills installed.")
stdin.write("\x1B") // Escape
await delay()
await waitForMockToBeCalled(mockOnClose)
expect(mockOnClose).toHaveBeenCalled()
})
@@ -79,11 +119,11 @@ describe("SkillsPanelContent", () => {
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
const { stdin, lastFrame } = render(<SkillsPanelContent {...defaultProps} />)
await waitForSkillsPanelReady(lastFrame, "test-skill")
stdin.write("\r") // Enter
await delay()
await waitForMockToBeCalled(mockOnUseSkill)
expect(mockOnUseSkill).toHaveBeenCalledWith("/test/path/SKILL.md")
})
@@ -94,11 +134,11 @@ describe("SkillsPanelContent", () => {
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
const { stdin, lastFrame } = render(<SkillsPanelContent {...defaultProps} />)
await waitForSkillsPanelReady(lastFrame, "test-skill")
stdin.write(" ") // Space
await delay()
await waitForMockToBeCalled(mockToggleSkill)
expect(mockToggleSkill).toHaveBeenCalledWith(
mockController,
@@ -116,15 +156,17 @@ describe("SkillsPanelContent", () => {
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
const { stdin, lastFrame } = render(<SkillsPanelContent {...defaultProps} />)
await waitForSkillsPanelReady(lastFrame, "skill")
// Navigate down to marketplace (past the one skill)
stdin.write("\x1B[B") // Down arrow
await delay()
// Use vim-style navigation here because it's more deterministic in the
// full suite than raw arrow escape sequences on Windows.
stdin.write("j")
await waitForFrameToInclude(lastFrame, " Browse more skills at https://skills.sh/")
stdin.write("\r") // Enter
await delay()
await waitForMockToBeCalled(mockExec)
// Should have called exec with open command
expect(mockExec).toHaveBeenCalled()
@@ -141,16 +183,16 @@ describe("SkillsPanelContent", () => {
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
const { stdin, lastFrame } = render(<SkillsPanelContent {...defaultProps} />)
await waitForSkillsPanelReady(lastFrame, "skill-1")
// Navigate down
stdin.write("\x1B[B") // Down arrow
await delay()
await waitForFrameToInclude(lastFrame, " ● skill-2")
// Press Enter - should use second skill
stdin.write("\r")
await delay()
await waitForMockToBeCalled(mockOnUseSkill)
expect(mockOnUseSkill).toHaveBeenCalledWith("/path2")
})
@@ -164,16 +206,16 @@ describe("SkillsPanelContent", () => {
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
const { stdin, lastFrame } = render(<SkillsPanelContent {...defaultProps} />)
await waitForSkillsPanelReady(lastFrame, "skill-1")
// Navigate down with j
stdin.write("j")
await delay()
await waitForFrameToInclude(lastFrame, " ● skill-2")
// Press Enter - should use second skill
stdin.write("\r")
await delay()
await waitForMockToBeCalled(mockOnUseSkill)
expect(mockOnUseSkill).toHaveBeenCalledWith("/path2")
})
@@ -186,10 +228,11 @@ describe("SkillsPanelContent", () => {
mockToggleSkill.mockRejectedValueOnce(new Error("toggle failed"))
const { stdin, lastFrame } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
await waitForSkillsPanelReady(lastFrame, "test-skill")
stdin.write(" ") // Space to toggle
await delay(100)
await waitForMockToBeCalled(mockToggleSkill)
await waitForFrameToInclude(lastFrame, "● test-skill")
// toggleSkill was called with enabled: false (toggled from true)
expect(mockToggleSkill).toHaveBeenCalledWith(mockController, expect.objectContaining({ enabled: false }))
@@ -204,15 +247,15 @@ describe("SkillsPanelContent", () => {
localSkills: [],
})
const { stdin } = render(<SkillsPanelContent {...defaultProps} />)
await delay()
const { stdin, lastFrame } = render(<SkillsPanelContent {...defaultProps} />)
await waitForSkillsPanelReady(lastFrame, "only-skill")
// Navigate up from first item (should wrap to last - marketplace)
stdin.write("\x1B[A") // Up arrow
await delay()
await waitForFrameToInclude(lastFrame, " Browse more skills at https://skills.sh/")
stdin.write("\r") // Enter
await delay()
await waitForMockToBeCalled(mockExec)
// Should have opened marketplace (wrapped to last item)
expect(mockExec).toHaveBeenCalled()
@@ -221,8 +264,9 @@ describe("SkillsPanelContent", () => {
describe("skill loading", () => {
it("should call refreshSkills on mount", async () => {
render(<SkillsPanelContent {...defaultProps} />)
await delay()
const { lastFrame } = render(<SkillsPanelContent {...defaultProps} />)
await waitForMockToBeCalled(mockRefreshSkills)
await waitForFrameToExclude(lastFrame, "Loading skills...")
expect(mockRefreshSkills).toHaveBeenCalled()
})
+37 -9
View File
@@ -6,13 +6,13 @@
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 React, { useCallback, useEffect, useMemo, useRef, 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 { isEnterKey, isMouseEscapeSequence } from "../utils/input"
import { Panel } from "./Panel"
const SKILLS_MARKETPLACE_URL = "https://skills.sh/"
@@ -38,6 +38,14 @@ export const SkillsPanelContent: React.FC<SkillsPanelContentProps> = ({ controll
const [localSkills, setLocalSkills] = useState<SkillInfo[]>([])
const [selectedIndex, setSelectedIndex] = useState(0)
const [isLoading, setIsLoading] = useState(true)
const inputStateRef = useRef({
isLoading: true,
selectedIndex: 0,
skillEntries: [] as Array<{ skill: SkillInfo; isGlobal: boolean }>,
})
const handleToggleRef = useRef<() => Promise<void>>(async () => {})
const handleUseRef = useRef<() => void>(() => {})
const openMarketplaceRef = useRef<() => void>(() => {})
// Load skills on mount
useEffect(() => {
@@ -58,8 +66,12 @@ export const SkillsPanelContent: React.FC<SkillsPanelContentProps> = ({ controll
// 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 }))
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)
@@ -117,6 +129,14 @@ export const SkillsPanelContent: React.FC<SkillsPanelContentProps> = ({ controll
}
})
}, [])
handleToggleRef.current = handleToggle
handleUseRef.current = handleUse
openMarketplaceRef.current = openMarketplace
inputStateRef.current = {
isLoading,
selectedIndex,
skillEntries,
}
// Total items = skills + 1 for marketplace link
const totalItems = skillEntries.length + 1
@@ -132,6 +152,14 @@ export const SkillsPanelContent: React.FC<SkillsPanelContentProps> = ({ controll
return
}
const { isLoading, selectedIndex, skillEntries } = inputStateRef.current
if (isLoading) {
return
}
const totalItems = skillEntries.length + 1
const isMarketplaceSelected = selectedIndex === skillEntries.length
// Navigation
if (key.upArrow || input === "k") {
setSelectedIndex((i) => (i > 0 ? i - 1 : totalItems - 1))
@@ -143,16 +171,16 @@ export const SkillsPanelContent: React.FC<SkillsPanelContentProps> = ({ controll
}
// Actions
if (key.return) {
if (isEnterKey(input, key)) {
if (isMarketplaceSelected) {
openMarketplace()
openMarketplaceRef.current()
} else {
handleUse()
handleUseRef.current()
}
return
}
if (input === " " && !isMarketplaceSelected) {
handleToggle()
void handleToggleRef.current()
return
}
},
@@ -248,7 +276,7 @@ const SkillRow: React.FC<{ skill: SkillInfo; isSelected: boolean }> = ({ skill,
{skill.description && (
<Box marginLeft={4}>
<Text color="gray">
{skill.description.length > 60 ? skill.description.slice(0, 57) + "..." : skill.description}
{skill.description.length > 60 ? `${skill.description.slice(0, 57)}...` : skill.description}
</Text>
</Box>
)}
+9 -3
View File
@@ -4,7 +4,9 @@
import { Box, Text, useInput } from "ink"
import React, { useEffect, useMemo, useState } from "react"
import { StateManager } from "@/core/storage/StateManager"
import { COLORS } from "../constants/colors"
import { FeatureTip } from "./FeatureTip"
interface ThinkingIndicatorProps {
mode?: "act" | "plan"
@@ -52,6 +54,7 @@ const ShimmerText: React.FC<{ text: string; color: string; shimmerPos: number }>
}
export const ThinkingIndicator: React.FC<ThinkingIndicatorProps> = ({ mode = "act", startTime, onCancel }) => {
const showFeatureTips = StateManager.get().getGlobalSettingsKey("showFeatureTips") ?? true
const message = mode === "plan" ? "Planning" : "Acting"
const color = mode === "plan" ? "yellow" : COLORS.primaryBlue
@@ -118,9 +121,12 @@ export const ThinkingIndicator: React.FC<ThinkingIndicatorProps> = ({ mode = "ac
}, [startTime, elapsedMs])
return (
<Box paddingLeft={1}>
<ShimmerText color={color} shimmerPos={shimmerPos} text={fullText} />
{elapsedStr && <Text color="gray"> ({elapsedStr} · esc to interrupt)</Text>}
<Box flexDirection="column">
<Box paddingLeft={1}>
<ShimmerText color={color} shimmerPos={shimmerPos} text={fullText} />
{elapsedStr && <Text color="gray"> ({elapsedStr} · esc to interrupt)</Text>}
</Box>
{showFeatureTips && <FeatureTip />}
</Box>
)
}
-333
View File
@@ -1,333 +0,0 @@
/**
* Welcome view component
* Shows an interactive prompt when user starts cline without a command
* Supports file mentions with @
*/
import { Box, Text, useInput } from "ink"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { StateManager } from "@/core/storage/StateManager"
import type { ApiProvider } from "@/shared/api"
import { getProviderDefaultModelId, getProviderModelIdKey, Mode, SettingsKey } from "@/shared/storage"
import { useStdinContext } from "../context/StdinContext"
import {
checkAndWarnRipgrepMissing,
extractMentionQuery,
type FileSearchResult,
getRipgrepInstallInstructions,
insertMention,
searchWorkspaceFiles,
} from "../utils/file-search"
import { isMouseEscapeSequence } from "../utils/input"
import { parseImagesFromInput } from "../utils/parser"
import { AccountInfoView } from "./AccountInfoView"
import { FileMentionMenu } from "./FileMentionMenu"
interface WelcomeViewProps {
onSubmit: (prompt: string, imagePaths: string[]) => void
onExit?: () => void
controller?: any
}
// ASCII art Cline logo
const CLINE_LOGO = [
" ::::::: ",
" ::::::::: ",
" ::::::::::::::::: ",
" ::::::::::::::::::::::: ",
" ::::::::::::::::::::::::: ",
" ::::::::::::::::::::::::::: ",
" ::::::: ::::::: ::::::: ",
" ::::::: ::::: ::::::: ",
":::::::: ::::: ::::::::",
":::::::: ::::: ::::::::",
" ::::::: ::::: ::::::: ",
" ::::::: ::::::: ::::::: ",
" ::::::::::::::::::::::::::: ",
" ::::::::::::::::::::::::: ",
" ::::::::::::::::::::::: ",
" :::::::::::::::: ",
]
const SEARCH_DEBOUNCE_MS = 150
const RIPGREP_WARNING_DURATION_MS = 5000
const MAX_SEARCH_RESULTS = 15
export const WelcomeView: React.FC<WelcomeViewProps> = ({ onSubmit, onExit, controller }) => {
const { isRawModeSupported } = useStdinContext()
const [textInput, setTextInput] = useState("")
const [fileResults, setFileResults] = useState<FileSearchResult[]>([])
const [selectedIndex, setSelectedIndex] = useState(0)
const [isSearching, setIsSearching] = useState(false)
const [showRipgrepWarning, setShowRipgrepWarning] = useState(false)
const [escPressedOnce, setEscPressedOnce] = useState(false)
const [mode, setMode] = useState<Mode>(() => {
const stateManager = StateManager.get()
return stateManager.getGlobalSettingsKey("mode") || "act"
})
const provider = useMemo(() => {
const stateManager = StateManager.get()
const mode = stateManager.getGlobalSettingsKey("mode") as string
const providerKey = mode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const currentProvider = stateManager.getGlobalSettingsKey(providerKey) as string
return currentProvider || "cline"
}, [controller])
// Get model ID based on current mode and provider
// Different providers use different state keys (e.g., cline uses actModeOpenRouterModelId)
const modelId = useMemo(() => {
const stateManager = StateManager.get()
const modelKey = getProviderModelIdKey(provider as ApiProvider, mode)
return (
(stateManager.getGlobalSettingsKey(modelKey as SettingsKey) as string) ||
getProviderDefaultModelId(provider as ApiProvider)
)
}, [mode, provider])
const toggleMode = useCallback(() => {
const newMode: Mode = mode === "act" ? "plan" : "act"
setMode(newMode)
const stateManager = StateManager.get()
stateManager.setGlobalState("mode", newMode)
}, [mode])
const refs = useRef({
searchTimeout: null as NodeJS.Timeout | null,
lastQuery: "",
hasCheckedRipgrep: false,
})
const { prompt, imagePaths } = parseImagesFromInput(textInput)
const mentionInfo = useMemo(() => extractMentionQuery(textInput), [textInput])
const workspacePath = useMemo(() => {
try {
const root = controller?.getWorkspaceManagerSync?.()?.getPrimaryRoot?.()
if (root?.path) {
return root.path
}
} catch {
// Fallback to cwd
}
return process.cwd()
}, [controller])
// Search for files when in mention mode
useEffect(() => {
const { current: r } = refs
if (!mentionInfo.inMentionMode) {
setFileResults([])
setSelectedIndex(0)
if (r.searchTimeout) {
clearTimeout(r.searchTimeout)
r.searchTimeout = null
}
return
}
// Check for ripgrep on first mention trigger
if (!r.hasCheckedRipgrep) {
r.hasCheckedRipgrep = true
if (checkAndWarnRipgrepMissing()) {
setShowRipgrepWarning(true)
setTimeout(() => setShowRipgrepWarning(false), RIPGREP_WARNING_DURATION_MS)
}
}
const { query } = mentionInfo
if (query === r.lastQuery) {
return
}
r.lastQuery = query
if (r.searchTimeout) {
clearTimeout(r.searchTimeout)
}
setIsSearching(true)
r.searchTimeout = setTimeout(async () => {
try {
const results = await searchWorkspaceFiles(query, workspacePath, MAX_SEARCH_RESULTS)
setFileResults(results)
setSelectedIndex(0)
} catch {
setFileResults([])
} finally {
setIsSearching(false)
}
}, SEARCH_DEBOUNCE_MS)
return () => {
if (r.searchTimeout) {
clearTimeout(r.searchTimeout)
}
}
}, [mentionInfo.inMentionMode, mentionInfo.query, workspacePath])
useInput(
(input, key) => {
// Filter out mouse escape sequences
if (isMouseEscapeSequence(input)) {
return
}
const inMenu = mentionInfo.inMentionMode && fileResults.length > 0
// Menu navigation
if (inMenu) {
if (key.upArrow) {
setSelectedIndex((i) => (i > 0 ? i - 1 : fileResults.length - 1))
return
}
if (key.downArrow) {
setSelectedIndex((i) => (i < fileResults.length - 1 ? i + 1 : 0))
return
}
if (key.tab || key.return) {
const file = fileResults[selectedIndex]
if (file) {
setTextInput(insertMention(textInput, mentionInfo.atIndex, file.path))
setFileResults([])
setSelectedIndex(0)
}
return
}
if (key.escape) {
setFileResults([])
setSelectedIndex(0)
return
}
}
// Normal input handling
if (key.tab && !mentionInfo.inMentionMode) {
toggleMode()
return
}
if (key.return && !mentionInfo.inMentionMode) {
if (prompt.trim() || imagePaths.length > 0) {
onSubmit(prompt.trim(), imagePaths)
}
return
}
if (key.escape && !mentionInfo.inMentionMode) {
if (escPressedOnce) {
onExit?.()
} else {
setEscPressedOnce(true)
}
return
}
if (key.backspace || key.delete) {
setTextInput((prev) => prev.slice(0, -1))
setEscPressedOnce(false)
return
}
if (input && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow && !key.tab) {
setTextInput((prev) => prev + input)
setEscPressedOnce(false)
}
},
{ isActive: isRawModeSupported },
)
const borderColor = mode === "act" ? "blue" : "yellow"
return (
<Box flexDirection="column" width="100%">
{/* Account/Provider info at top */}
{controller && (
<Box marginBottom={1}>
<AccountInfoView controller={controller} />
</Box>
)}
{/* Cline logo - centered */}
<Box alignItems="center" flexDirection="column">
{CLINE_LOGO.map((line, idx) => (
// biome-ignore lint/suspicious/noArrayIndexKey: static array that never changes
<Text color="white" key={idx}>
{line}
</Text>
))}
</Box>
{/* Main prompt - centered, bold */}
<Box justifyContent="center" marginTop={1}>
<Text bold color="white">
What can I do for you?
</Text>
</Box>
{/* Ripgrep warning if needed */}
{showRipgrepWarning && (
<Box marginTop={1}>
<Text color="yellow"> ripgrep not found - file search will be slower. </Text>
<Text color="gray">Install: {getRipgrepInstallInstructions()}</Text>
</Box>
)}
{/* Input field with border */}
<Box
borderColor={borderColor}
borderStyle="round"
flexDirection="row"
marginTop={1}
paddingLeft={1}
paddingRight={1}
width="100%">
<Text>{textInput}</Text>
<Text inverse> </Text>
</Box>
{/* Model ID and Mode toggle row */}
<Box justifyContent="space-between" width="100%">
{/* Model ID on left */}
<Text color="gray">{modelId}</Text>
{/* Mode toggle on right */}
<Box gap={1}>
<Box>
<Text bold={mode === "plan"} color={mode === "plan" ? "yellow" : "gray"}>
{mode === "plan" ? "●" : "○"} Plan
</Text>
</Box>
<Box>
<Text bold={mode === "act"} color={mode === "act" ? "blue" : "gray"}>
{mode === "act" ? "●" : "○"} Act
</Text>
</Box>
<Text color="gray">(Tab)</Text>
</Box>
</Box>
{/* File mention menu - below input */}
{mentionInfo.inMentionMode && (
<FileMentionMenu
isLoading={isSearching}
query={mentionInfo.query}
results={fileResults}
selectedIndex={selectedIndex}
/>
)}
{/* Attached images */}
{imagePaths.length > 0 && (
<Text color="magenta">
📎 {imagePaths.length} image{imagePaths.length > 1 ? "s" : ""} attached
</Text>
)}
{/* Help text */}
<Box>
<Text color="gray">Enter to submit · @ to mention files · </Text>
<Text bold={escPressedOnce} color={escPressedOnce ? "white" : "gray"}>
{escPressedOnce ? "Press Esc again to exit" : "Esc to exit"}
</Text>
</Box>
</Box>
)
}
+3 -1
View File
@@ -79,7 +79,7 @@ export class CliDiffServiceClient implements DiffServiceClientInterface {
* CLI implementation of EnvService - handles environment operations
*/
export class CliEnvServiceClient implements EnvServiceClientInterface {
private clipboardContent: string = ""
private clipboardContent = ""
private getTelemetrySetting(): proto.host.Setting {
// Read from StateManager - defaults to ENABLED if not set or "unset"
@@ -102,6 +102,8 @@ export class CliEnvServiceClient implements EnvServiceClientInterface {
version: CLI_VERSION,
platform: "Cline CLI - Node.js",
clineType: ClineClient.Cli,
// remoteName is intentionally omitted — the CLI runs locally on the user's machine.
// If CLI-in-container scenarios arise, populate this field to enable remote cadence tuning.
})
}
+3 -64
View File
@@ -1,71 +1,10 @@
/**
* 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.
* The previous programmatic agent API has been removed.
* This module is intentionally empty for package compatibility.
*
* @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"
export {}
+25 -3
View File
@@ -26,6 +26,9 @@ import { useCallback, useEffect, useRef, useState } from "react"
* to unmount and remount everything from scratch. This resets Ink's internal tracking
* AND re-renders Static content since the components are brand new instances.
*
* We only run this full recovery when terminal width changes. Height-only resizes do not
* affect wrapping in the same way and should not restart the task view.
*
* Gemini CLI does the same thing in AppContainer.tsx: debounce 300ms, then
* stdout.write(ansiEscapes.clearTerminal) + setHistoryRemountKey(prev => prev + 1).
*
@@ -41,6 +44,8 @@ export function useTerminalSize() {
})
const [resizeKey, setResizeKey] = useState(0)
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const previousColumnsRef = useRef(process.stdout.columns || 80)
const pendingWidthRefreshRef = useRef(false)
const refreshAfterResize = useCallback(() => {
// Clear terminal + scrollback to wipe stale content from old width
@@ -56,17 +61,33 @@ export function useTerminalSize() {
useEffect(() => {
function updateSize() {
const nextColumns = process.stdout.columns || 80
const nextRows = process.stdout.rows || 24
const didWidthChange = nextColumns !== previousColumnsRef.current
previousColumnsRef.current = nextColumns
setSize({
columns: process.stdout.columns || 80,
rows: process.stdout.rows || 24,
columns: nextColumns,
rows: nextRows,
})
if (didWidthChange) {
pendingWidthRefreshRef.current = true
}
if (!pendingWidthRefreshRef.current) {
return
}
// Debounce: wait 300ms after last resize event to do full recovery
if (debounceRef.current) {
clearTimeout(debounceRef.current)
}
debounceRef.current = setTimeout(() => {
refreshAfterResize()
if (pendingWidthRefreshRef.current) {
refreshAfterResize()
pendingWidthRefreshRef.current = false
}
debounceRef.current = null
}, 300)
}
@@ -76,6 +97,7 @@ export function useTerminalSize() {
if (debounceRef.current) {
clearTimeout(debounceRef.current)
}
pendingWidthRefreshRef.current = false
}
}, [refreshAfterResize])
+217 -27
View File
@@ -1,5 +1,6 @@
import { Command } from "commander"
import { beforeEach, describe, expect, it } from "vitest"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { captureUnhandledException } from "."
/**
* Tests for CLI command parsing and structure
@@ -10,6 +11,22 @@ import { beforeEach, describe, expect, it } from "vitest"
describe("CLI Commands", () => {
let program: Command
function getCommand(name: string): Command {
const command = program.commands.find((candidate) => candidate.name() === name)
if (!command) {
throw new Error(`Missing command: ${name}`)
}
return command
}
function getSubcommand(commandName: string, subcommandName: string): Command {
const subcommand = getCommand(commandName).commands.find((candidate) => candidate.name() === subcommandName)
if (!subcommand) {
throw new Error(`Missing subcommand: ${commandName} ${subcommandName}`)
}
return subcommand
}
beforeEach(() => {
// Create a fresh program instance for each test
program = new Command()
@@ -25,6 +42,7 @@ describe("CLI Commands", () => {
.option("-a, --act", "Run in act mode")
.option("-p, --plan", "Run in plan mode")
.option("-y, --yolo", "Enable yolo mode")
.option("--auto-approve-all", "Enable auto-approve all")
.option("-m, --model <model>", "Model to use")
.option("-i, --images <paths...>", "Image file paths")
.option("-v, --verbose", "Show verbose output")
@@ -33,6 +51,9 @@ describe("CLI Commands", () => {
.option("--thinking [tokens]", "Enable extended thinking")
.option("--reasoning-effort <effort>", "Reasoning effort")
.option("--max-consecutive-mistakes <count>", "Maximum consecutive mistakes")
.option("--double-check-completion", "Reject first completion attempt to force re-verification")
.option("--auto-condense", "Enable AI-powered context compaction instead of mechanical truncation")
.option("--hooks-dir <path>", "Additional hooks directory")
.action(() => {})
program
@@ -62,6 +83,28 @@ describe("CLI Commands", () => {
.option("--config <path>", "Configuration directory")
.action(() => {})
const mcpCommand = program.command("mcp").description("Manage MCP servers")
mcpCommand
.command("add")
.description("Add an MCP server shortcut")
.argument("<name>", "MCP server name")
.argument("[targetOrCommand...]", "Command args for stdio, or URL for remote")
.option("--type <type>", "Transport type", "stdio")
.option("-c, --cwd <path>", "Working directory")
.option("--config <path>", "Configuration directory")
.action(() => {})
program
.command("kanban")
.description("Run kanban")
.action(() => {})
program
.command("update")
.description("Check for updates and install if available")
.option("-v, --verbose", "Show verbose output")
.action(() => {})
// Default command for interactive mode
program
.argument("[prompt]", "Task prompt")
@@ -72,6 +115,13 @@ describe("CLI Commands", () => {
.option("--thinking [tokens]", "Enable extended thinking")
.option("--reasoning-effort <effort>", "Reasoning effort")
.option("--max-consecutive-mistakes <count>", "Maximum consecutive mistakes")
.option("--double-check-completion", "Reject first completion attempt to force re-verification")
.option("--auto-condense", "Enable AI-powered context compaction instead of mechanical truncation")
.option("--hooks-dir <path>", "Additional hooks directory")
.option("--auto-approve-all", "Enable auto-approve all")
.option("--update", "Check for updates and install if available")
.option("--kanban", "Run kanban")
.option("--tui", "Open the legacy terminal UI instead of the kanban experience")
.action(() => {})
})
@@ -88,91 +138,119 @@ describe("CLI Commands", () => {
})
it("should parse --act flag", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const taskCmd = getCommand("task")
const args = ["test prompt", "--act"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().act).toBe(true)
})
it("should parse --plan flag", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const taskCmd = getCommand("task")
const args = ["test prompt", "--plan"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().plan).toBe(true)
})
it("should parse --yolo flag", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const taskCmd = getCommand("task")
const args = ["test prompt", "--yolo"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().yolo).toBe(true)
})
it("should parse --auto-approve-all flag", () => {
const taskCmd = getCommand("task")
const args = ["test prompt", "--auto-approve-all"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().autoApproveAll).toBe(true)
})
it("should parse --model option", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const taskCmd = getCommand("task")
const args = ["test prompt", "--model", "claude-sonnet-4-20250514"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().model).toBe("claude-sonnet-4-20250514")
})
it("should parse --images option with multiple paths", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const taskCmd = getCommand("task")
const args = ["test prompt", "--images", "/path/to/img1.png", "/path/to/img2.jpg"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().images).toEqual(["/path/to/img1.png", "/path/to/img2.jpg"])
})
it("should parse --verbose flag", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const taskCmd = getCommand("task")
const args = ["test prompt", "--verbose"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().verbose).toBe(true)
})
it("should parse --cwd option", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const taskCmd = getCommand("task")
const args = ["test prompt", "--cwd", "/some/path"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().cwd).toBe("/some/path")
})
it("should parse --config option", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const taskCmd = getCommand("task")
const args = ["test prompt", "--config", "/custom/config"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().config).toBe("/custom/config")
})
it("should parse --thinking flag", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const taskCmd = getCommand("task")
const args = ["test prompt", "--thinking"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().thinking).toBe(true)
})
it("should parse --thinking with token budget", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const taskCmd = getCommand("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 taskCmd = getCommand("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 taskCmd = getCommand("task")
const args = ["test prompt", "--max-consecutive-mistakes", "999"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().maxConsecutiveMistakes).toBe("999")
})
it("should parse --hooks-dir option", () => {
const taskCmd = getCommand("task")
const args = ["test prompt", "--hooks-dir", "/tmp/hooks"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().hooksDir).toBe("/tmp/hooks")
})
it("should parse --double-check-completion flag", () => {
const taskCmd = getCommand("task")
const args = ["test prompt", "--double-check-completion"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().doubleCheckCompletion).toBe(true)
})
it("should parse --auto-condense flag", () => {
const taskCmd = getCommand("task")
const args = ["test prompt", "--auto-condense"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().autoCondense).toBe(true)
})
it("should parse short flags", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const taskCmd = getCommand("task")
const args = ["test prompt", "-a", "-v", "-m", "gpt-4"]
taskCmd.parse(args, { from: "user" })
expect(taskCmd.opts().act).toBe(true)
@@ -183,26 +261,26 @@ describe("CLI Commands", () => {
describe("history command", () => {
it("should have default limit of 10", () => {
const historyCmd = program.commands.find((c) => c.name() === "history")!
const historyCmd = getCommand("history")
historyCmd.parse([], { from: "user" })
expect(historyCmd.opts().limit).toBe("10")
})
it("should have default page of 1", () => {
const historyCmd = program.commands.find((c) => c.name() === "history")!
const historyCmd = getCommand("history")
historyCmd.parse([], { from: "user" })
expect(historyCmd.opts().page).toBe("1")
})
it("should parse --limit option", () => {
const historyCmd = program.commands.find((c) => c.name() === "history")!
const historyCmd = getCommand("history")
const args = ["--limit", "20"]
historyCmd.parse(args, { from: "user" })
expect(historyCmd.opts().limit).toBe("20")
})
it("should parse --page option", () => {
const historyCmd = program.commands.find((c) => c.name() === "history")!
const historyCmd = getCommand("history")
const args = ["--page", "3"]
historyCmd.parse(args, { from: "user" })
expect(historyCmd.opts().page).toBe("3")
@@ -215,7 +293,7 @@ describe("CLI Commands", () => {
})
it("should parse short flags", () => {
const historyCmd = program.commands.find((c) => c.name() === "history")!
const historyCmd = getCommand("history")
const args = ["-n", "5", "-p", "2"]
historyCmd.parse(args, { from: "user" })
expect(historyCmd.opts().limit).toBe("5")
@@ -230,13 +308,34 @@ describe("CLI Commands", () => {
})
it("should parse --config option", () => {
const configCmd = program.commands.find((c) => c.name() === "config")!
const configCmd = getCommand("config")
const args = ["--config", "/custom/path"]
configCmd.parse(args, { from: "user" })
expect(configCmd.opts().config).toBe("/custom/path")
})
})
describe("kanban command", () => {
it("should parse kanban command", () => {
const args = ["node", "cli", "kanban"]
program.parse(args)
})
})
describe("update command", () => {
it("should parse update command", () => {
const args = ["node", "cli", "update"]
program.parse(args)
})
it("should parse --verbose on update command", () => {
const updateCmd = getCommand("update")
const args = ["--verbose"]
updateCmd.parse(args, { from: "user" })
expect(updateCmd.opts().verbose).toBe(true)
})
})
describe("auth command", () => {
it("should parse auth command", () => {
const args = ["node", "cli", "auth"]
@@ -244,35 +343,35 @@ describe("CLI Commands", () => {
})
it("should parse --provider option", () => {
const authCmd = program.commands.find((c) => c.name() === "auth")!
const authCmd = getCommand("auth")
const args = ["--provider", "openai"]
authCmd.parse(args, { from: "user" })
expect(authCmd.opts().provider).toBe("openai")
})
it("should parse --apikey option", () => {
const authCmd = program.commands.find((c) => c.name() === "auth")!
const authCmd = getCommand("auth")
const args = ["--apikey", "sk-test-key"]
authCmd.parse(args, { from: "user" })
expect(authCmd.opts().apikey).toBe("sk-test-key")
})
it("should parse --modelid option", () => {
const authCmd = program.commands.find((c) => c.name() === "auth")!
const authCmd = getCommand("auth")
const args = ["--modelid", "gpt-4"]
authCmd.parse(args, { from: "user" })
expect(authCmd.opts().modelid).toBe("gpt-4")
})
it("should parse --baseurl option", () => {
const authCmd = program.commands.find((c) => c.name() === "auth")!
const authCmd = getCommand("auth")
const args = ["--baseurl", "https://api.example.com"]
authCmd.parse(args, { from: "user" })
expect(authCmd.opts().baseurl).toBe("https://api.example.com")
})
it("should parse short flags", () => {
const authCmd = program.commands.find((c) => c.name() === "auth")!
const authCmd = getCommand("auth")
const args = ["-p", "anthropic", "-k", "key123", "-m", "claude-sonnet-4-20250514"]
authCmd.parse(args, { from: "user" })
expect(authCmd.opts().provider).toBe("anthropic")
@@ -281,6 +380,30 @@ describe("CLI Commands", () => {
})
})
describe("mcp command", () => {
it("should parse mcp add stdio syntax", () => {
const args = ["node", "cli", "mcp", "add", "kanban", "--", "kanban", "mcp"]
program.parse(args)
})
it("should parse mcp add remote http syntax", () => {
const args = ["node", "cli", "mcp", "add", "linear", "https://mcp.linear.app/mcp", "--type", "http"]
program.parse(args)
})
it("should default mcp add type to stdio", () => {
const addCmd = getSubcommand("mcp", "add")
addCmd.parse(["kanban", "--", "kanban", "mcp"], { from: "user" })
expect(addCmd.opts().type).toBe("stdio")
})
it("should parse mcp add type option", () => {
const addCmd = getSubcommand("mcp", "add")
addCmd.parse(["linear", "https://mcp.linear.app/mcp", "--type", "http"], { from: "user" })
expect(addCmd.opts().type).toBe("http")
})
})
describe("default command (interactive mode)", () => {
it("should parse optional prompt argument", () => {
const args = ["node", "cli", "do something"]
@@ -321,6 +444,31 @@ describe("CLI Commands", () => {
program.parse(["node", "cli", "--max-consecutive-mistakes", "7"])
expect(program.opts().maxConsecutiveMistakes).toBe("7")
})
it("should parse --hooks-dir option", () => {
program.parse(["node", "cli", "--hooks-dir", "/tmp/hooks"])
expect(program.opts().hooksDir).toBe("/tmp/hooks")
})
it("should parse --auto-approve-all flag", () => {
program.parse(["node", "cli", "--auto-approve-all"])
expect(program.opts().autoApproveAll).toBe(true)
})
it("should parse --kanban flag", () => {
program.parse(["node", "cli", "--kanban"])
expect(program.opts().kanban).toBe(true)
})
it("should parse --update flag", () => {
program.parse(["node", "cli", "--update"])
expect(program.opts().update).toBe(true)
})
it("should parse --tui flag", () => {
program.parse(["node", "cli", "--tui"])
expect(program.opts().tui).toBe(true)
})
})
describe("command structure", () => {
@@ -330,11 +478,14 @@ describe("CLI Commands", () => {
expect(commandNames).toContain("history")
expect(commandNames).toContain("config")
expect(commandNames).toContain("auth")
expect(commandNames).toContain("mcp")
expect(commandNames).toContain("kanban")
expect(commandNames).toContain("update")
})
it("should have correct aliases", () => {
const taskCmd = program.commands.find((c) => c.name() === "task")!
const historyCmd = program.commands.find((c) => c.name() === "history")!
const taskCmd = getCommand("task")
const historyCmd = getCommand("history")
expect(taskCmd.aliases()).toContain("t")
expect(historyCmd.aliases()).toContain("h")
})
@@ -410,3 +561,42 @@ describe("getProviderModelIdKey", () => {
expect(getProviderModelIdKey("unknown-provider", "act")).toBeNull()
})
})
const mockCaptureException = vi.fn().mockResolvedValue(undefined)
const mockDispose = vi.fn().mockResolvedValue(undefined)
vi.mock("@/services/error/ErrorService", () => {
return {
ErrorService: {
get: () => ({
captureException: mockCaptureException,
dispose: mockDispose,
}),
},
}
})
describe("captureUnhandledException", () => {
beforeEach(() => {
vi.resetAllMocks()
})
it("captures unhandled exceptions", async () => {
const testError = new Error("Test unhandled exception")
await captureUnhandledException(testError, "unhandledRejection")
expect(mockCaptureException).toHaveBeenCalledWith(testError, { context: "unhandledRejection" })
expect(mockDispose).toHaveBeenCalled()
})
it("does not throw if captureException fails", async () => {
mockCaptureException.mockRejectedValueOnce(new Error("Capture failed"))
const testError = new Error("Test unhandled exception")
await expect(captureUnhandledException(testError, "unhandledRejection")).resolves.not.toThrow()
expect(mockCaptureException).toHaveBeenCalledWith(testError, { context: "unhandledRejection" })
expect(mockDispose).not.toHaveBeenCalled()
})
})
+380 -39
View File
@@ -2,6 +2,7 @@
* Cline CLI - TypeScript implementation with React Ink
*/
import type { ChildProcess } from "node:child_process"
import { exit } from "node:process"
import type { ApiProvider } from "@shared/api"
import { Command } from "commander"
@@ -9,6 +10,8 @@ import { render } from "ink"
import React from "react"
import { ClineEndpoint } from "@/config"
import type { Controller } from "@/core/controller"
import { getHooksEnabledSafe } from "@/core/hooks/hooks-utils"
import { setRuntimeHooksDir } from "@/core/storage/disk"
import { StateManager } from "@/core/storage/StateManager"
import { AuthHandler } from "@/hosts/external/AuthHandler"
import { HostProvider } from "@/hosts/host-provider"
@@ -23,8 +26,8 @@ import { Session } from "@/shared/services/Session"
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"
import { KanbanMigrationView } from "./components/KanbanMigrationView"
import { checkRawModeSupport } from "./context/StdinContext"
import { createCliHostBridgeProvider } from "./controllers"
import { CliCommentReviewController } from "./controllers/CliCommentReviewController"
@@ -32,6 +35,21 @@ import { CliWebviewProvider } from "./controllers/CliWebviewProvider"
import { isAuthConfigured } from "./utils/auth"
import { restoreConsole, suppressConsoleUnlessVerbose } from "./utils/console"
import { printInfo, printWarning } from "./utils/display"
import {
forwardSignalToKanbanProcess,
isKanbanCommandAvailable,
KANBAN_LAUNCH_COMMAND,
KANBAN_SHUTDOWN_TIMEOUT_MS,
type KanbanMigrationAction,
LEGACY_TUI_FLAG,
markKanbanMigrationAnnouncementShown,
resolveKanbanInstallCommand,
shouldLaunchKanbanByDefault,
shouldShowKanbanMigrationAnnouncementForCurrentUser,
spawnKanbanInstallProcess,
spawnKanbanProcess,
} from "./utils/kanban"
import { addMcpServerShortcut, type McpAddOptions } from "./utils/mcp"
import { selectOutputMode } from "./utils/mode-selection"
import { parseImagesFromInput, processImagePaths } from "./utils/parser"
import { CLINE_CLI_DIR, getCliBinaryPath } from "./utils/path"
@@ -39,6 +57,7 @@ import { readStdinIfPiped } from "./utils/piped"
import { runPlainTextTask } from "./utils/plain-text-task"
import { applyProviderConfig } from "./utils/provider-config"
import { getValidCliProviders, isValidCliProvider } from "./utils/providers"
import { findMostRecentTaskForWorkspace } from "./utils/task-history"
import { autoUpdateOnStartup, checkForUpdates } from "./utils/update"
import { initializeCliContext } from "./vscode-context"
import { CLI_LOG_FILE, shutdownEvent, window } from "./vscode-shim"
@@ -53,18 +72,24 @@ suppressConsoleUnlessVerbose()
interface TaskOptions {
act?: boolean
plan?: boolean
kanban?: boolean
tui?: boolean
model?: string
verbose?: boolean
cwd?: string
continue?: boolean
config?: string
thinking?: boolean | string
reasoningEffort?: string
maxConsecutiveMistakes?: string
yolo?: boolean
autoApproveAll?: boolean
doubleCheckCompletion?: boolean
autoCondense?: boolean
timeout?: string
json?: boolean
stdinWasPiped?: boolean
hooksDir?: string
}
let telemetryDisposed = false
@@ -133,46 +158,43 @@ function normalizeMaxConsecutiveMistakes(value?: string): number | undefined {
function applyTaskOptions(options: TaskOptions): void {
// Apply mode flag
if (options.plan) {
StateManager.get().setGlobalState("mode", "plan")
StateManager.get().setSessionOverride("mode", "plan")
telemetryService.captureHostEvent("mode_flag", "plan")
} else if (options.act) {
StateManager.get().setGlobalState("mode", "act")
StateManager.get().setSessionOverride("mode", "act")
telemetryService.captureHostEvent("mode_flag", "act")
}
// Apply model override if specified
if (options.model) {
const selectedMode = (StateManager.get().getGlobalSettingsKey("mode") || "act") as "act" | "plan"
const selectedMode = (StateManager.get().getGlobalSettingsKey("mode") ?? "act") as "act" | "plan"
const providerKey = selectedMode === "act" ? "actModeApiProvider" : "planModeApiProvider"
const currentProvider = StateManager.get().getGlobalSettingsKey(providerKey) as ApiProvider
const modelKey = getProviderModelIdKey(currentProvider, selectedMode)
if (modelKey) {
StateManager.get().setGlobalState(modelKey, options.model)
StateManager.get().setSessionOverride(modelKey, options.model)
}
telemetryService.captureHostEvent("model_flag", options.model)
}
const currentMode = (StateManager.get().getGlobalSettingsKey("mode") || "act") as "act" | "plan"
// Set thinking budget based on --thinking flag (boolean or number)
let thinkingBudget = 0
if (options.thinking) {
if (options.thinking !== undefined) {
let thinkingBudget = 1024
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) {
setModeScopedState(currentMode, (mode) => {
const thinkingKey = mode === "act" ? "actModeThinkingBudgetTokens" : "planModeThinkingBudgetTokens"
StateManager.get().setSessionOverride(thinkingKey, thinkingBudget)
})
telemetryService.captureHostEvent("thinking_flag", "true")
}
@@ -180,14 +202,14 @@ function applyTaskOptions(options: TaskOptions): void {
if (reasoningEffort !== undefined) {
setModeScopedState(currentMode, (mode) => {
const reasoningKey = mode === "act" ? "actModeReasoningEffort" : "planModeReasoningEffort"
StateManager.get().setGlobalState(reasoningKey, reasoningEffort)
StateManager.get().setSessionOverride(reasoningKey, reasoningEffort)
})
telemetryService.captureHostEvent("reasoning_effort_flag", reasoningEffort)
}
const maxConsecutiveMistakes = normalizeMaxConsecutiveMistakes(options.maxConsecutiveMistakes)
if (maxConsecutiveMistakes !== undefined) {
StateManager.get().setGlobalState("maxConsecutiveMistakes", maxConsecutiveMistakes)
StateManager.get().setSessionOverride("maxConsecutiveMistakes", maxConsecutiveMistakes)
telemetryService.captureHostEvent("max_consecutive_mistakes_flag", String(maxConsecutiveMistakes))
}
@@ -198,11 +220,22 @@ function applyTaskOptions(options: TaskOptions): void {
telemetryService.captureHostEvent("yolo_flag", "true")
}
// Set auto-approve-all as a session-scoped override so CLI flag does not
// persist user settings to disk.
if (options.autoApproveAll) {
StateManager.get().setSessionOverride("autoApproveAllToggled", true)
telemetryService.captureHostEvent("auto_approve_all_flag", "true")
}
// Set double-check completion based on flag
if (options.doubleCheckCompletion) {
StateManager.get().setGlobalState("doubleCheckCompletionEnabled", true)
StateManager.get().setSessionOverride("doubleCheckCompletionEnabled", true)
telemetryService.captureHostEvent("double_check_completion_flag", "true")
}
if (options.autoCondense) {
StateManager.get().setSessionOverride("useAutoCondense", true)
}
}
/**
@@ -233,6 +266,83 @@ function getPlainTextModeReason(options: TaskOptions): string {
return getModeSelection(options).reason
}
function runKanbanAlias(spawnOptions?: Parameters<typeof spawnKanbanProcess>[0]): void {
const launchKanban = () => {
const child = spawnKanbanProcess(spawnOptions)
activeKanbanProcess = child
child.on("error", (error) => {
clearActiveKanbanProcess()
const errorMessage = error instanceof Error ? ` ${error.message}` : ""
printWarning(`Failed to run '${KANBAN_LAUNCH_COMMAND}'.${errorMessage}`)
exit(1)
})
child.on("close", (code, signal) => {
clearActiveKanbanProcess()
exit(resolveProcessExitCode(code, signal))
})
}
if (isKanbanCommandAvailable()) {
launchKanban()
return
}
const installCommand = resolveKanbanInstallCommand()
if (!installCommand) {
printWarning(
`'${KANBAN_LAUNCH_COMMAND}' not found and no supported package manager was detected in PATH (npm, pnpm, bun). Install Kanban globally and try again.`,
)
exit(1)
}
const installProcess = spawnKanbanInstallProcess(installCommand)
installProcess.on("error", (error) => {
const errorMessage = error instanceof Error ? ` ${error.message}` : ""
printWarning(`Failed to run '${installCommand.displayCommand}'.${errorMessage}`)
exit(1)
})
installProcess.on("close", (code, signal) => {
const installExitCode = resolveProcessExitCode(code, signal)
if (installExitCode !== 0) {
printWarning(`Failed to install Kanban automatically. Please run '${installCommand.displayCommand}' manually.`)
exit(installExitCode)
}
launchKanban()
})
}
async function showKanbanMigrationView(): Promise<KanbanMigrationAction> {
let selectedAction: KanbanMigrationAction = "exit"
await runInkApp(
React.createElement(KanbanMigrationView, {
isRawModeSupported: checkRawModeSupport(),
onSelect: (action: KanbanMigrationAction) => {
selectedAction = action
},
}),
async () => {},
)
return selectedAction
}
async function addMcpServer(name: string, targetOrCommand: string[] = [], options: McpAddOptions): Promise<void> {
try {
const result = await addMcpServerShortcut(name, targetOrCommand, options)
const transportLabel = result.transportType === "streamableHttp" ? "http" : result.transportType
printInfo(`Added MCP server '${result.serverName}' (${transportLabel}) to ${result.settingsPath}`)
} catch (error) {
printWarning(error instanceof Error ? error.message : "Failed to add MCP server.")
exit(1)
}
}
/**
* Run a task in plain text mode (no Ink UI).
* Handles auth check, task execution, cleanup, and exit.
@@ -299,6 +409,62 @@ let activeContext: CliContext | null = null
let isShuttingDown = false
// Track if we're in plain text mode (no Ink UI) - set by runTask when piped stdin detected
let isPlainTextMode = false
let activeKanbanProcess: ChildProcess | null = null
let activeKanbanShutdownTimer: NodeJS.Timeout | null = null
function clearActiveKanbanProcess(): void {
activeKanbanProcess = null
if (activeKanbanShutdownTimer) {
clearTimeout(activeKanbanShutdownTimer)
activeKanbanShutdownTimer = null
}
}
function requestKanbanProcessShutdown(signal: NodeJS.Signals): void {
if (!activeKanbanProcess) {
return
}
forwardSignalToKanbanProcess({
child: activeKanbanProcess,
signal,
})
if (activeKanbanShutdownTimer) {
clearTimeout(activeKanbanShutdownTimer)
}
if (signal === "SIGKILL") {
activeKanbanShutdownTimer = null
return
}
activeKanbanShutdownTimer = setTimeout(() => {
if (!activeKanbanProcess) {
return
}
forwardSignalToKanbanProcess({
child: activeKanbanProcess,
signal: "SIGKILL",
})
}, KANBAN_SHUTDOWN_TIMEOUT_MS)
activeKanbanShutdownTimer.unref?.()
}
function resolveProcessExitCode(code: number | null, signal: NodeJS.Signals | null): number {
if (code !== null) {
return code
}
switch (signal) {
case "SIGINT":
return 130
case "SIGTERM":
return 143
default:
return 1
}
}
/**
* Wait for stdout to fully drain before exiting.
@@ -316,8 +482,55 @@ async function drainStdout(): Promise<void> {
})
}
export async function captureUnhandledException(reason: Error, context: string) {
try {
// ErrorService may not be initialized yet (e.g., error occurred before initializeCli())
// so we guard with a try/get pattern rather than letting ErrorService.get() throw
let errorService: ErrorService | null = null
try {
errorService = ErrorService.get()
} catch {
// ErrorService not yet initialized; skip capture
}
if (errorService) {
await errorService.captureException(reason, { context })
// dispose flushes any pending error captures to ensure they're sent before the process exits
return errorService.dispose()
}
} catch {
// Ignore errors during shutdown to avoid an infinite loop
Logger.info("Error capturing unhandled exception. Proceeding with shutdown.")
}
}
const EXIT_TIMEOUT_MS = 3000
function onUnhandledException(reason: unknown, context: string) {
Logger.error("Unhandled exception:", reason)
const finalError = reason instanceof Error ? reason : new Error(String(reason))
restoreConsole()
console.error(finalError)
setTimeout(() => process.exit(1), EXIT_TIMEOUT_MS)
captureUnhandledException(finalError, context).finally(() => {
process.exit(1)
})
}
function setupSignalHandlers() {
const shutdown = async (signal: string) => {
if (activeKanbanProcess) {
if (isShuttingDown) {
requestKanbanProcessShutdown("SIGKILL")
return
}
isShuttingDown = true
requestKanbanProcessShutdown(signal === "SIGTERM" ? "SIGTERM" : "SIGINT")
return
}
if (isShuttingDown) {
// Force exit on second signal
process.exit(1)
@@ -353,7 +566,11 @@ function setupSignalHandlers() {
} catch {
// StateManager may not be initialized yet
}
await ErrorService.get().dispose()
try {
await ErrorService.get().dispose()
} catch {
// ErrorService may not be initialized yet
}
await disposeTelemetryServices()
}
} catch {
@@ -375,9 +592,14 @@ function setupSignalHandlers() {
Logger.info("Suppressed unhandled rejection due to abort:", message)
return
}
// For other unhandled rejections, log to file via Logger (if available)
// For other unhandled rejections, capture the exception and log to file via Logger (if available)
// This won't show in terminal but will be in log files for debugging
Logger.error("Unhandled rejection:", reason)
onUnhandledException(reason, "unhandledRejection")
})
process.on("uncaughtException", (reason: unknown) => {
onUnhandledException(reason, "uncaughtException")
})
}
@@ -394,6 +616,7 @@ interface CliContext {
interface InitOptions {
config?: string
cwd?: string
hooksDir?: string
verbose?: boolean
enableAuth?: boolean
}
@@ -403,6 +626,7 @@ interface InitOptions {
*/
async function initializeCli(options: InitOptions): Promise<CliContext> {
const workspacePath = options.cwd || process.cwd()
setRuntimeHooksDir(options.hooksDir)
const { extensionContext, storageContext, DATA_DIR, EXTENSION_DIR } = initializeCliContext({
clineDir: options.config,
workspaceDir: workspacePath,
@@ -504,6 +728,11 @@ async function runTask(prompt: string, options: TaskOptions & { images?: string[
// Task without prompt starts in interactive mode
telemetryService.captureHostEvent("task_command", prompt ? "task" : "interactive")
// Capture piped stdin telemetry now that HostProvider is initialized
if (options.stdinWasPiped) {
telemetryService.captureHostEvent("piped", "detached")
}
// Apply shared task options (mode, model, thinking, yolo)
applyTaskOptions(options)
await StateManager.get().flushPendingState()
@@ -596,7 +825,7 @@ async function showConfig(options: { config?: string }) {
dataDir: ctx.dataDir,
globalState: stateManager.getAllGlobalStateEntries(),
workspaceState: stateManager.getAllWorkspaceStateEntries(),
hooksEnabled: true,
hooksEnabled: getHooksEnabledSafe(stateManager.getGlobalSettingsKey("hooksEnabled")),
skillsEnabled: true,
isRawModeSupported: checkRawModeSupport(),
}),
@@ -728,6 +957,7 @@ 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("--auto-approve-all", "Enable auto-approve all actions while keeping interactive mode")
.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")
@@ -738,6 +968,8 @@ program
.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("--auto-condense", "Enable AI-powered context compaction instead of mechanical truncation")
.option("--hooks-dir <path>", "Path to additional hooks directory for runtime hook injection")
.option("-T, --taskId <id>", "Resume an existing task by ID")
.action((prompt, options) => {
if (options.taskId) {
@@ -773,6 +1005,18 @@ program
.option("--config <path>", "Path to Cline configuration directory")
.action(runAuth)
const mcpCommand = program.command("mcp").description("Manage MCP servers")
mcpCommand
.command("add")
.description("Add an MCP server shortcut to cline_mcp_settings.json")
.argument("<name>", "MCP server name")
.argument("[targetOrCommand...]", "For stdio: use -- <command> [args]. For http/sse: provide <url>.")
.option("--type <type>", "Transport type: stdio (default), http, or sse", "stdio")
.option("-c, --cwd <path>", "Working directory for config resolution")
.option("--config <path>", "Path to Cline configuration directory")
.action(addMcpServer)
program
.command("version")
.description("Show Cline CLI version number")
@@ -782,7 +1026,12 @@ program
.command("update")
.description("Check for updates and install if available")
.option("-v, --verbose", "Show verbose output")
.action(() => checkForUpdates(CLI_VERSION))
.action((options) => checkForUpdates(CLI_VERSION, { verbose: options.verbose, includeKanban: true }))
program
.command("kanban")
.description(`Run ${KANBAN_LAUNCH_COMMAND}`)
.action(() => runKanbanAlias())
// Dev command with subcommands
const devCommand = program.command("dev").description("Developer tools and utilities")
@@ -808,8 +1057,8 @@ function findTaskInHistory(taskId: string): HistoryItem | null {
* Resume an existing task by ID
* Loads the task and optionally prefills the input with a prompt
*/
async function resumeTask(taskId: string, options: TaskOptions & { initialPrompt?: string }) {
const ctx = await initializeCli({ ...options, enableAuth: true })
async function resumeTask(taskId: string, options: TaskOptions & { initialPrompt?: string }, existingContext?: CliContext) {
const ctx = existingContext || (await initializeCli({ ...options, enableAuth: true }))
// Validate task exists
const historyItem = findTaskInHistory(taskId)
@@ -822,6 +1071,11 @@ async function resumeTask(taskId: string, options: TaskOptions & { initialPrompt
telemetryService.captureHostEvent("resume_task_command", options.initialPrompt ? "with_prompt" : "interactive")
// Capture piped stdin telemetry now that HostProvider is initialized
if (options.stdinWasPiped) {
telemetryService.captureHostEvent("piped", "detached")
}
// Apply shared task options (mode, model, thinking, yolo)
applyTaskOptions(options)
await StateManager.get().flushPendingState()
@@ -856,16 +1110,35 @@ async function resumeTask(taskId: string, options: TaskOptions & { initialPrompt
)
}
async function continueTask(options: TaskOptions) {
const ctx = await initializeCli({ ...options, enableAuth: true })
const historyItem = findMostRecentTaskForWorkspace(StateManager.get().getGlobalStateKey("taskHistory"), ctx.workspacePath)
if (!historyItem) {
printWarning(`No previous task found for ${ctx.workspacePath}`)
printInfo("Start a new task or use 'cline history' to browse previous tasks.")
await disposeCliContext(ctx)
exit(1)
}
return resumeTask(historyItem.id, options, ctx)
}
/**
* Show welcome prompt and wait for user input
* If auth is not configured, show auth flow first
*/
async function showWelcome(options: { verbose?: boolean; cwd?: string; config?: string; thinking?: boolean }) {
async function showWelcome(options: TaskOptions) {
const ctx = await initializeCli({ ...options, enableAuth: true })
// Check if auth is configured
const hasAuth = await isAuthConfigured()
// Apply CLI task options in interactive startup too, so flags like
// --auto-approve-all and --yolo affect the initial TUI state.
applyTaskOptions(options)
await StateManager.get().flushPendingState()
let hadError = false
await runInkApp(
@@ -895,6 +1168,7 @@ 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("--auto-approve-all", "Enable auto-approve all actions while keeping interactive mode")
.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")
@@ -905,16 +1179,36 @@ program
.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("--auto-condense", "Enable AI-powered context compaction instead of mechanical truncation")
.option("--hooks-dir <path>", "Path to additional hooks directory for runtime hook injection")
.option("--update", "Check for updates and install if available")
.option("--kanban", `Run ${KANBAN_LAUNCH_COMMAND}`)
.option("--tui", "Open the legacy terminal UI instead of the kanban experience")
.option("-T, --taskId <id>", "Resume an existing task by ID")
.option("--continue", "Resume the most recent task from the current working directory")
.action(async (prompt, options) => {
// Check for ACP mode first - this takes precedence over everything else
if (options.acp) {
await runAcpMode({
config: options.config,
cwd: options.cwd,
verbose: options.verbose,
})
if (options.kanban && options.tui) {
printWarning(`Use either --kanban or ${LEGACY_TUI_FLAG}, not both.`)
exit(1)
}
if (options.update) {
if (prompt || options.taskId || options.continue || options.kanban || options.tui) {
printWarning("Use --update without a prompt or task flags.")
exit(1)
}
await checkForUpdates(CLI_VERSION, { verbose: options.verbose, includeKanban: true })
return
}
if (options.kanban) {
if (prompt) {
printWarning("Use --kanban without a prompt.")
exit(1)
}
runKanbanAlias({ cwd: options.cwd })
return
}
@@ -927,6 +1221,53 @@ program
// stdinInput has content means stdin was piped with data
const stdinWasPiped = stdinInput !== null
if (
shouldLaunchKanbanByDefault({
prompt,
stdinWasPiped,
taskId: options.taskId,
continue: options.continue,
tui: options.tui,
})
) {
let migrationAction: "kanban" | "exit" = "kanban"
const ctx = await initializeCli({ ...options, enableAuth: true })
try {
if (await shouldShowKanbanMigrationAnnouncementForCurrentUser()) {
migrationAction = await showKanbanMigrationView()
await markKanbanMigrationAnnouncementShown()
}
} finally {
await disposeCliContext(ctx)
}
if (migrationAction === "exit") {
exit(0)
}
runKanbanAlias({ cwd: options.cwd })
return
}
if (options.taskId && options.continue) {
printWarning("Use either --taskId or --continue, not both.")
exit(1)
}
if (options.continue) {
if (prompt) {
printWarning("Use --continue without a prompt.")
exit(1)
}
if (stdinWasPiped) {
printWarning("Use --continue without piped input.")
exit(1)
}
await continueTask(options)
return
}
// Error if stdin was piped but empty AND no prompt was provided
// This handles:
// - `echo "" | cline` -> error (empty stdin, no prompt)
@@ -947,8 +1288,6 @@ program
effectivePrompt = stdinInput
}
telemetryService.captureHostEvent("piped", "detached")
// Debug: show that we received piped input
if (options.verbose) {
process.stderr.write(`[debug] Received ${stdinInput.length} bytes from stdin\n`)
@@ -975,4 +1314,6 @@ program
})
// Parse and run
program.parse()
if (process.env.VITEST !== "true") {
program.parse()
}
+10
View File
@@ -15,3 +15,13 @@ export function isMouseEscapeSequence(input: string): boolean {
// They contain [< followed by numbers, semicolons, and end with M or m
return input.includes("[<") && /\[<\d+;\d+;\d+[Mm]/.test(input)
}
/**
* Ink's key metadata can be inconsistent across platforms/test environments for Enter.
* In particular, some Windows CI/test runs surface Enter as raw "\r" input without
* setting key.return. Treat either representation as Enter so keyboard handlers remain
* stable in production and in tests across platforms.
*/
export function isEnterKey(input: string, key: { return?: boolean }): boolean {
return key.return === true || input === "\r" || input === "\n"
}
+265
View File
@@ -0,0 +1,265 @@
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { describe, expect, it, vi } from "vitest"
import {
buildKanbanInstallSpawnOptions,
buildKanbanSpawnOptions,
forwardSignalToKanbanProcess,
hasUsedLegacyCli,
isKanbanCommandAvailable,
resolveKanbanInstallCommand,
shouldDetachKanbanProcess,
shouldLaunchKanbanByDefault,
shouldShowKanbanMigrationAnnouncement,
} from "./kanban"
describe("shouldLaunchKanbanByDefault", () => {
it("launches kanban for a bare interactive run", () => {
expect(
shouldLaunchKanbanByDefault({
stdinWasPiped: false,
}),
).toBe(true)
})
it("does not launch kanban when a prompt is provided", () => {
expect(
shouldLaunchKanbanByDefault({
prompt: "fix the tests",
stdinWasPiped: false,
}),
).toBe(false)
})
it("does not launch kanban when stdin is piped", () => {
expect(
shouldLaunchKanbanByDefault({
stdinWasPiped: true,
}),
).toBe(false)
})
it("does not launch kanban when the legacy tui is requested", () => {
expect(
shouldLaunchKanbanByDefault({
stdinWasPiped: false,
tui: true,
}),
).toBe(false)
})
})
describe("hasUsedLegacyCli", () => {
it("treats task history as legacy usage", () => {
expect(
hasUsedLegacyCli({
taskHistoryCount: 1,
isNewUser: true,
welcomeViewCompleted: undefined,
hasConfiguredAuth: false,
}),
).toBe(true)
})
it("treats configured auth as legacy usage", () => {
expect(
hasUsedLegacyCli({
taskHistoryCount: 0,
isNewUser: true,
welcomeViewCompleted: undefined,
hasConfiguredAuth: true,
}),
).toBe(true)
})
it("skips the announcement for fresh installs", () => {
expect(
hasUsedLegacyCli({
taskHistoryCount: 0,
isNewUser: true,
welcomeViewCompleted: undefined,
hasConfiguredAuth: false,
}),
).toBe(false)
})
})
describe("kanban process launch", () => {
it("detaches the kanban process on unix-like platforms", () => {
expect(shouldDetachKanbanProcess("darwin")).toBe(true)
expect(shouldDetachKanbanProcess("linux")).toBe(true)
})
it("keeps the kanban process attached on windows", () => {
expect(shouldDetachKanbanProcess("win32")).toBe(false)
})
it("uses a detached process group by default on unix-like platforms", () => {
expect(buildKanbanSpawnOptions({}, "darwin")).toMatchObject({
stdio: "inherit",
detached: true,
})
})
it("does not detach the process on windows", () => {
expect(buildKanbanSpawnOptions({}, "win32")).toMatchObject({
stdio: "inherit",
detached: false,
})
})
it("enables shell mode on windows for command launches", () => {
expect(buildKanbanSpawnOptions({}, "win32")).toMatchObject({
shell: true,
})
})
it("does not set shell mode on unix-like platforms", () => {
expect(buildKanbanSpawnOptions({}, "darwin")).not.toHaveProperty("shell")
})
})
describe("kanban command availability", () => {
it("returns false when PATH is empty", () => {
expect(isKanbanCommandAvailable({ PATH: "" }, "darwin")).toBe(false)
})
it("detects the kanban command in PATH", () => {
const tempDirectory = mkdtempSync(join(tmpdir(), "kanban-cli-test-"))
const commandPath = join(tempDirectory, process.platform === "win32" ? "kanban.cmd" : "kanban")
writeFileSync(commandPath, process.platform === "win32" ? "@echo off\r\necho ok\r\n" : "#!/bin/sh\necho ok\n")
if (process.platform !== "win32") {
chmodSync(commandPath, 0o755)
}
try {
expect(isKanbanCommandAvailable({ PATH: tempDirectory }, process.platform)).toBe(true)
} finally {
rmSync(tempDirectory, { recursive: true, force: true })
}
})
})
describe("kanban install process launch", () => {
it("does not detach the install process on unix-like platforms", () => {
expect(buildKanbanInstallSpawnOptions({}, "darwin")).toMatchObject({
stdio: "inherit",
detached: false,
})
})
it("enables shell mode on windows for npm.cmd launches", () => {
expect(buildKanbanInstallSpawnOptions({}, "win32")).toMatchObject({
shell: true,
})
})
})
describe("kanban installer resolution", () => {
it("prefers npm when available", () => {
const tempDirectory = mkdtempSync(join(tmpdir(), "kanban-installer-test-"))
writeFileSync(join(tempDirectory, "npm"), "#!/bin/sh\necho npm\n")
writeFileSync(join(tempDirectory, "pnpm"), "#!/bin/sh\necho pnpm\n")
writeFileSync(join(tempDirectory, "bun"), "#!/bin/sh\necho bun\n")
chmodSync(join(tempDirectory, "npm"), 0o755)
chmodSync(join(tempDirectory, "pnpm"), 0o755)
chmodSync(join(tempDirectory, "bun"), 0o755)
try {
expect(resolveKanbanInstallCommand({ PATH: tempDirectory }, "darwin")?.packageManager).toBe("npm")
} finally {
rmSync(tempDirectory, { recursive: true, force: true })
}
})
it("falls back to pnpm when npm is unavailable", () => {
const tempDirectory = mkdtempSync(join(tmpdir(), "kanban-installer-test-"))
writeFileSync(join(tempDirectory, "pnpm"), "#!/bin/sh\necho pnpm\n")
chmodSync(join(tempDirectory, "pnpm"), 0o755)
try {
const installer = resolveKanbanInstallCommand({ PATH: tempDirectory }, "darwin")
expect(installer?.packageManager).toBe("pnpm")
expect(installer?.displayCommand).toBe("pnpm add -g kanban@latest")
} finally {
rmSync(tempDirectory, { recursive: true, force: true })
}
})
it("falls back to bun when npm and pnpm are unavailable", () => {
const tempDirectory = mkdtempSync(join(tmpdir(), "kanban-installer-test-"))
writeFileSync(join(tempDirectory, "bun"), "#!/bin/sh\necho bun\n")
chmodSync(join(tempDirectory, "bun"), 0o755)
try {
const installer = resolveKanbanInstallCommand({ PATH: tempDirectory }, "darwin")
expect(installer?.packageManager).toBe("bun")
expect(installer?.displayCommand).toBe("bun add -g kanban@latest")
} finally {
rmSync(tempDirectory, { recursive: true, force: true })
}
})
it("returns null when no supported package manager is available", () => {
expect(resolveKanbanInstallCommand({ PATH: "" }, "darwin")).toBeNull()
})
})
describe("forwardSignalToKanbanProcess", () => {
it("signals the detached kanban process group on unix-like platforms", () => {
const killProcess = vi.fn()
const child = {
pid: 4321,
kill: vi.fn(),
}
forwardSignalToKanbanProcess({
child,
signal: "SIGINT",
platform: "darwin",
killProcess,
})
expect(killProcess).toHaveBeenCalledWith(-4321, "SIGINT")
expect(child.kill).not.toHaveBeenCalled()
})
it("signals the child process directly on windows", () => {
const killProcess = vi.fn()
const child = {
pid: 4321,
kill: vi.fn(),
}
forwardSignalToKanbanProcess({
child,
signal: "SIGTERM",
platform: "win32",
killProcess,
})
expect(killProcess).not.toHaveBeenCalled()
expect(child.kill).toHaveBeenCalledWith("SIGTERM")
})
})
describe("shouldShowKanbanMigrationAnnouncement", () => {
it("shows the announcement once for legacy users", () => {
expect(
shouldShowKanbanMigrationAnnouncement({
announcementShown: false,
hasUsedLegacyCli: true,
}),
).toBe(true)
})
it("does not show the announcement twice", () => {
expect(
shouldShowKanbanMigrationAnnouncement({
announcementShown: true,
hasUsedLegacyCli: true,
}),
).toBe(false)
})
})
+246
View File
@@ -0,0 +1,246 @@
import { type ChildProcess, type SpawnOptions, spawn } from "node:child_process"
import { accessSync, constants as fsConstants } from "node:fs"
import { delimiter, extname, join } from "node:path"
import { StateManager } from "@/core/storage/StateManager"
import { checkAnyProviderConfigured } from "./auth"
export const KANBAN_LAUNCH_COMMAND = "kanban"
export const KANBAN_SHUTDOWN_TIMEOUT_MS = 10_000
export const LEGACY_TUI_FLAG = "--tui"
export type KanbanMigrationAction = "kanban" | "exit"
type KanbanInstaller = "npm" | "pnpm" | "bun"
interface KanbanInstallCommand {
packageManager: KanbanInstaller
command: string
args: readonly string[]
displayCommand: string
}
interface SignalableKanbanProcess {
pid?: number
kill: (signal?: NodeJS.Signals | number) => boolean
}
function getKanbanCommand(platform: NodeJS.Platform = process.platform): string {
return platform === "win32" ? "kanban.cmd" : "kanban"
}
function getPackageManagerCommand(packageManager: KanbanInstaller, platform: NodeJS.Platform = process.platform): string {
if (platform !== "win32") {
return packageManager
}
return packageManager === "bun" ? "bun" : `${packageManager}.cmd`
}
const KANBAN_INSTALL_COMMANDS: ReadonlyArray<Omit<KanbanInstallCommand, "displayCommand">> = [
{
packageManager: "npm",
command: "npm",
args: ["install", "-g", "kanban@latest"],
},
{
packageManager: "pnpm",
command: "pnpm",
args: ["add", "-g", "kanban@latest"],
},
{
packageManager: "bun",
command: "bun",
args: ["add", "-g", "kanban@latest"],
},
]
function toDisplayCommand(command: string, args: readonly string[]): string {
return `${command} ${args.join(" ")}`
}
export function shouldDetachKanbanProcess(platform: NodeJS.Platform = process.platform): boolean {
return platform !== "win32"
}
export function buildKanbanSpawnOptions(options: SpawnOptions = {}, platform: NodeJS.Platform = process.platform): SpawnOptions {
return {
stdio: "inherit",
detached: shouldDetachKanbanProcess(platform),
...(platform === "win32" ? { shell: true } : {}),
...options,
}
}
export function buildKanbanInstallSpawnOptions(
options: SpawnOptions = {},
platform: NodeJS.Platform = process.platform,
): SpawnOptions {
return {
stdio: "inherit",
detached: false,
...(platform === "win32" ? { shell: true } : {}),
...options,
}
}
export function spawnKanbanProcess(options: SpawnOptions = {}): ChildProcess {
return spawn(getKanbanCommand(), [], buildKanbanSpawnOptions(options))
}
export function spawnKanbanInstallProcess(installCommand: KanbanInstallCommand, options: SpawnOptions = {}): ChildProcess {
return spawn(
getPackageManagerCommand(installCommand.packageManager),
[...installCommand.args],
buildKanbanInstallSpawnOptions(options),
)
}
function getPathEntries(env: NodeJS.ProcessEnv): string[] {
const pathValue = env.PATH ?? env.Path ?? env.path
if (!pathValue) {
return []
}
return pathValue
.split(delimiter)
.map((entry) => entry.trim().replace(/^"(.*)"$/u, "$1"))
.filter((entry) => entry.length > 0)
}
function pathExists(candidatePath: string, platform: NodeJS.Platform): boolean {
try {
if (platform === "win32") {
accessSync(candidatePath, fsConstants.F_OK)
} else {
accessSync(candidatePath, fsConstants.X_OK)
}
return true
} catch {
return false
}
}
export function isCommandAvailable(
command: string,
env: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform,
): boolean {
const commandHasExtension = extname(command).length > 0
const pathExtensions =
platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter((ext) => ext.length > 0) : []
for (const pathEntry of getPathEntries(env)) {
const commandPath = join(pathEntry, command)
if (pathExists(commandPath, platform)) {
return true
}
if (!commandHasExtension && platform === "win32") {
for (const extension of pathExtensions) {
if (pathExists(`${commandPath}${extension}`, platform)) {
return true
}
}
}
}
return false
}
export function isKanbanCommandAvailable(
env: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform,
): boolean {
return isCommandAvailable(getKanbanCommand(platform), env, platform)
}
export function resolveKanbanInstallCommand(
env: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform,
): KanbanInstallCommand | null {
for (const installCommand of KANBAN_INSTALL_COMMANDS) {
if (isCommandAvailable(installCommand.command, env, platform)) {
return {
...installCommand,
displayCommand: toDisplayCommand(installCommand.command, installCommand.args),
}
}
}
return null
}
export function forwardSignalToKanbanProcess(options: {
child: SignalableKanbanProcess
signal: NodeJS.Signals
platform?: NodeJS.Platform
killProcess?: (pid: number, signal: NodeJS.Signals | number) => boolean
}): void {
if (options.child.pid == null) {
return
}
if (shouldDetachKanbanProcess(options.platform)) {
try {
;(options.killProcess ?? process.kill)(-options.child.pid, options.signal)
return
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "ESRCH") {
return
}
}
}
options.child.kill(options.signal)
}
export function shouldLaunchKanbanByDefault(options: {
prompt?: string
stdinWasPiped: boolean
taskId?: string
continue?: boolean
tui?: boolean
}): boolean {
return !options.prompt && !options.stdinWasPiped && !options.taskId && !options.continue && !options.tui
}
export function hasUsedLegacyCli(options: {
taskHistoryCount: number
isNewUser: boolean
welcomeViewCompleted: boolean | undefined
hasConfiguredAuth: boolean
}): boolean {
return (
options.taskHistoryCount > 0 ||
options.isNewUser === false ||
options.welcomeViewCompleted !== undefined ||
options.hasConfiguredAuth
)
}
export function shouldShowKanbanMigrationAnnouncement(options: {
announcementShown: boolean
hasUsedLegacyCli: boolean
}): boolean {
return !options.announcementShown && options.hasUsedLegacyCli
}
export async function shouldShowKanbanMigrationAnnouncementForCurrentUser(): Promise<boolean> {
const stateManager = StateManager.get()
const hasConfiguredAuth = await checkAnyProviderConfigured()
const hasUsedLegacy = hasUsedLegacyCli({
taskHistoryCount: stateManager.getGlobalStateKey("taskHistory")?.length ?? 0,
isNewUser: stateManager.getGlobalStateKey("isNewUser"),
welcomeViewCompleted: stateManager.getGlobalStateKey("welcomeViewCompleted"),
hasConfiguredAuth,
})
return shouldShowKanbanMigrationAnnouncement({
announcementShown: stateManager.getGlobalStateKey("cliKanbanMigrationAnnouncementShown"),
hasUsedLegacyCli: hasUsedLegacy,
})
}
export async function markKanbanMigrationAnnouncementShown(): Promise<void> {
const stateManager = StateManager.get()
stateManager.setGlobalState("cliKanbanMigrationAnnouncementShown", true)
await stateManager.flushPendingState()
}
+63
View File
@@ -0,0 +1,63 @@
import * as fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { afterEach, describe, expect, it } from "vitest"
import { addMcpServerShortcut } from "./mcp"
const tempDirs: string[] = []
async function createTempConfigDir(): Promise<string> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "cline-mcp-test-"))
tempDirs.push(dir)
return dir
}
type McpSettingsFile = {
mcpServers: Record<string, Record<string, unknown>>
}
async function readMcpSettings(configDir: string): Promise<McpSettingsFile> {
const settingsPath = path.join(configDir, "data", "settings", "cline_mcp_settings.json")
return JSON.parse(await fs.readFile(settingsPath, "utf-8")) as McpSettingsFile
}
afterEach(async () => {
for (const dir of tempDirs.splice(0, tempDirs.length)) {
await fs.rm(dir, { recursive: true, force: true })
}
})
describe("addMcpServerShortcut", () => {
it("writes stdio servers with type=stdio", async () => {
const configDir = await createTempConfigDir()
await addMcpServerShortcut("kanban", ["kanban", "mcp"], { config: configDir })
const settings = await readMcpSettings(configDir)
expect(settings.mcpServers.kanban).toEqual({
command: "kanban",
args: ["mcp"],
type: "stdio",
})
})
it("maps --type http to streamableHttp", async () => {
const configDir = await createTempConfigDir()
await addMcpServerShortcut("linear", ["https://mcp.linear.app/mcp"], { config: configDir, type: "http" })
const settings = await readMcpSettings(configDir)
expect(settings.mcpServers.linear).toEqual({
url: "https://mcp.linear.app/mcp",
type: "streamableHttp",
})
})
it("errors when URL is provided without --type http", async () => {
const configDir = await createTempConfigDir()
await expect(addMcpServerShortcut("linear", ["https://mcp.linear.app/mcp"], { config: configDir })).rejects.toThrow(
"Use --type http",
)
})
})
+159
View File
@@ -0,0 +1,159 @@
import * as fs from "node:fs/promises"
import path from "node:path"
import { getMcpSettingsFilePath } from "@/core/storage/disk"
import { ServerConfigSchema } from "@/services/mcp/schemas"
import { initializeCliContext } from "../vscode-context"
export interface McpAddOptions {
type?: string
config?: string
cwd?: string
}
export type McpAddTransportType = "stdio" | "streamableHttp" | "sse"
export interface AddMcpServerResult {
serverName: string
transportType: McpAddTransportType
settingsPath: string
}
function normalizeMcpTransportType(value?: string): McpAddTransportType {
const normalized = (value || "stdio").trim().toLowerCase()
switch (normalized) {
case "stdio":
return "stdio"
case "http":
case "streamable-http":
case "streamablehttp":
return "streamableHttp"
case "sse":
return "sse"
default:
throw new Error(`Invalid MCP transport type '${value}'. Valid values: stdio, http, sse.`)
}
}
function parseMcpSettings(content: string, settingsPath: string): Record<string, unknown> {
const trimmedContent = content.trim()
if (!trimmedContent) {
return { mcpServers: {} }
}
let parsed: unknown
try {
parsed = JSON.parse(content)
} catch {
throw new Error(`Invalid JSON in ${settingsPath}. Please fix the file and try again.`)
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`Invalid MCP settings file at ${settingsPath}. Expected a JSON object.`)
}
const settings = parsed as Record<string, unknown>
if (settings.mcpServers === undefined) {
settings.mcpServers = {}
}
if (!settings.mcpServers || typeof settings.mcpServers !== "object" || Array.isArray(settings.mcpServers)) {
throw new Error(`Invalid MCP settings file at ${settingsPath}. Expected 'mcpServers' to be an object.`)
}
return settings
}
function createMcpServerConfig(targetOrCommand: string[], transportType: McpAddTransportType): Record<string, unknown> {
if (transportType === "stdio") {
if (targetOrCommand.length < 1) {
throw new Error("Missing stdio command. Example: cline mcp add kanban -- kanban mcp")
}
// Guard against common mistake:
// `cline mcp add <name> <url>` without `--type http`
if (targetOrCommand.length === 1) {
const [value] = targetOrCommand
try {
const parsedUrl = new URL(value)
if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") {
throw new Error(
`Looks like you provided a URL for '${value}'. Use --type http, for example: cline mcp add <name> ${value} --type http`,
)
}
} catch (error) {
if (error instanceof Error && error.message.startsWith("Looks like you provided a URL")) {
throw error
}
}
}
const [command, ...args] = targetOrCommand
const config: Record<string, unknown> = {
command,
type: "stdio",
}
if (args.length > 0) {
config.args = args
}
ServerConfigSchema.parse(config)
return config
}
if (targetOrCommand.length !== 1) {
throw new Error(
"HTTP/SSE MCP servers require exactly one URL. Example: cline mcp add linear https://mcp.linear.app/mcp --type http",
)
}
const config = {
url: targetOrCommand[0],
type: transportType,
}
ServerConfigSchema.parse(config)
return config
}
export async function addMcpServerShortcut(
name: string,
targetOrCommand: string[] = [],
options: McpAddOptions,
): Promise<AddMcpServerResult> {
const trimmedName = name.trim()
if (!trimmedName) {
throw new Error("Server name is required.")
}
const transportType = normalizeMcpTransportType(options.type)
const { DATA_DIR } = initializeCliContext({
clineDir: options.config,
workspaceDir: options.cwd || process.cwd(),
})
const settingsDirectoryPath = path.join(DATA_DIR, "settings")
await fs.mkdir(settingsDirectoryPath, { recursive: true })
const settingsPath = await getMcpSettingsFilePath(settingsDirectoryPath)
const content = await fs.readFile(settingsPath, "utf-8")
const settings = parseMcpSettings(content, settingsPath)
const mcpServers = settings.mcpServers as Record<string, unknown>
if (mcpServers[trimmedName]) {
throw new Error(`An MCP server named '${trimmedName}' already exists.`)
}
const serverConfig = createMcpServerConfig(targetOrCommand, transportType)
mcpServers[trimmedName] = serverConfig
await fs.writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8")
return {
serverName: trimmedName,
transportType,
settingsPath,
}
}

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