Compare commits

...

752 Commits

Author SHA1 Message Date
Saoud Rizwan fd794ce3be chore(cli): release v3.0.52 2026-08-08 19:34:11 -07:00
Saoud Rizwan f5f1071af2 chore(sdk): release v0.0.72 2026-08-08 19:00:03 -07:00
Saoud Rizwan 4540390096 desktop: hide git jargon for non-git folders (CLIENTS-100) (#13059) 2026-08-08 15:57:00 -07:00
Saoud Rizwan b590e14b91 desktop: paste clipboard images into the composer (CLIENTS-78) (#13057)
* desktop: paste clipboard images into the composer as attachments (CLIENTS-78)

Pasting a screenshot into the composer did nothing: only drag-and-drop
and the paperclip file picker fed the attachment pipeline. Add an
onPaste handler on the composer textarea that extracts image files from
the clipboard, renames them to timestamped pasted-image-*.png files, and
routes them through the existing onAttachFiles flow. Text pastes are
untouched.

* desktop: only extract clipboard images in formats message serialization supports

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-08 15:56:27 -07:00
Saoud Rizwan 54cc156089 desktop: context-aware welcome suggestions for non-code folders (CLIENTS-98) (#13060)
* desktop: context-aware welcome suggestions for non-code folders (CLIENTS-98)

* desktop: treat pending branch discovery as its own state for welcome cards

The welcome-card classifier read the "no-git" sentinel as a confirmed
non-repo, but page.tsx also used that value for the initial state and
while a workspace switch was awaiting branch discovery, so a git repo
could briefly show the plain-folder cards. Branch state is now null
while discovery is pending: the welcome screen shows no cards until the
folder is classified, and chat-mode cards (which never depend on git
state) still show immediately. Other branch consumers keep the string
contract via a "no-git" fallback.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: carry nullable branch state to all consumers

Propagate the pending-discovery null through ChatInputBar,
WorkspaceSelector, and the welcome workspace controls instead of
coercing to "no-git" at the page boundary, so only display leaves
fall back and the welcome classifier is the single consumer that
distinguishes pending from confirmed non-repo.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-08 15:45:29 -07:00
Saoud Rizwan 6c599d18c3 desktop: surface folder picker failures and add manual path fallback (CLIENTS-73) (#13056)
* desktop: never let 'Add project…' fail silently; add manual folder path entry (CLIENTS-73)

- sidecar picker tries zenity then kdialog on Linux and throws a descriptive
  error when neither exists, instead of returning null (indistinguishable
  from user cancel); picked paths are trimmed of trailing separators
- picker failures now surface as visible error messages in both workspace
  selectors, with a manual path-entry fallback (typed absolute or ~ paths
  in the search box offer an 'Open folder' action)
- failed workspace switches (invalid/nonexistent paths) show an inline
  error instead of silently doing nothing
- validate_workspace_directory expands ~ and returns the resolved path

* desktop: keep workspace menu search/error state through catalog refreshes

The welcome-screen workspace picker reset its search text and error
message whenever onRefreshWorkspaces changed identity, which happens on
every session-history poll. Typing a path or reading an inline error
raced against the timer: the menu would silently wipe mid-interaction.
Hold the refresh callback in a ref so the reset only runs when the menu
actually opens.

* desktop: format welcome-workspace-controls test

* desktop: distinguish picker launch failures from user cancellation

A zenity/kdialog rejection with a non-ENOENT spawn error (EACCES, EMFILE,
ENOMEM) or a crash signal was classified as a user cancel, which skipped
the kdialog fallback and suppressed the inline error - recreating the
silent no-op this branch is meant to eliminate. Only a clean exit code 1
from a dialog that actually opened now counts as cancellation; broken
backends fall through to the next candidate and surface a descriptive
error otherwise. Picker logic moved to sidecar/workspace-picker.ts with
an injectable exec so the classification is unit-tested.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Classify picker launch failures separately from user cancellation

zenity/kdialog failures like EACCES, EMFILE, or ENOMEM were treated as
user cancellation, suppressing the kdialog fallback and the inline
manual-entry error. Only a clean exit code 1 now counts as a cancel;
any other failure falls through to the next backend or throws the
picker-unavailable error. Picker logic moved to sidecar/folder-picker.ts
with an injectable exec so the classification is unit tested.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Revert "Classify picker launch failures separately from user cancellation"

This reverts commit 24d27a004d.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-08 15:43:52 -07:00
Saoud Rizwan 513aacc0e6 fix(core): full-stop semantics and abort-window queue edits for surviving queues (#13100)
* fix(core): preserve queued prompts across user-initiated aborts

Pressing stop while prompts were queued silently destroyed them:
abort() called clearAborted(), which emptied the pending prompt queue
with no way to recover the typed input. The prompts vanished from the
UI, were never sent, and left no trace in session artifacts.

Aborting now only stops the in-flight turn. Queued prompts stay in the
queue and drain once the abort settles, matching the drain behavior
that already existed for self-aborted turns (loop detector / mistake
limit). The thrown-abort path (completeAbortedInteractiveTurn) now
schedules the same drain that runTurn schedules for turns resolving
with an aborted finish.

* fix(core): full-stop semantics and abort-window queue edits for surviving queues

Follow-up to the queued-prompt survival change: aborting a user turn keeps
the queue and auto-runs it, but two gaps remained.

1. No full stop: aborting a queue-initiated turn also kept draining, so
   every Escape consumed one queued prompt and started a fresh provider
   call - a session with queued messages could never be brought to rest.
   Aborting a drained turn now discards the remaining queue: the first
   Escape skips to your queued follow-ups, a second Escape stops the
   queued work too.

2. Queue operations were still rejected while an abort settled: a prompt
   typed right after Escape was silently dropped, and queued prompts were
   briefly uneditable and undeletable even though they were about to
   auto-run. enqueue/update/delete now work during the abort window;
   scheduleDrain/drain still wait for the abort to settle.
2026-08-08 12:37:18 -07:00
Saoud Rizwan ff2f860941 test(core): pin abort + hub-restart durability, and title seeded sessions (#13097)
* test(core): cover abort + host restart + seeded recovery durability

Adds an e2e regression guard for the reported "cancel a turn, lose the
conversation" failure: a cancelled turn, a daemon restart, a
client-side recovery seeded from disk, and a second restart before that
replacement ever runs a turn. Reverting the eager seeded-history
persistence makes the final read come back empty.

Materializing a seeded session at start also left its history row with
no prompt and no title, since there is no first prompt to derive one
from. Seed the title from the inherited transcript using the same
inference listSessionHistory hydration applies, so forks and recoveries
stay identifiable in unhydrated surfaces too.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): retitle seeded sessions from their first user prompt

Eagerly-materialized seeded sessions kept the interim transcript-
inferred title forever, a behavior change from pre-eager persistence
where a fork's history row was titled by the first post-fork prompt.

The interim title now only covers the window where no turn has run
(previously those rows were simply absent), and the first user prompt
after the seed backfills the row's prompt and retitles it — unless the
user renamed the session in the meantime, in which case only the prompt
column is backfilled. The resident manifest and session metadata are
updated in step so the end-of-turn usage-metadata merge cannot clobber
the title back through a stale in-memory fallback.

The e2e mock's updateSession now mirrors the real persistence-service
contract (row + manifest file), and the durability e2e covers both the
retitle and the rename guard; removing the retitle call fails the
'now add tests' assertion.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(core): collapse seeded-session titling to the old mechanism

The interim transcript-derived title, retitle flags, rename comparison,
and resident-manifest syncing existed only to title forks that never
run a turn - a new nicety, not parity. Dropping it collapses the whole
design back to what rows did before eager persistence: the persistence
service derives the title from the prompt when a row gains one, so the
host only needs to backfill the promptless row with the first user
prompt via updateSession. Renames win automatically because the service
preserves an existing title when no explicit title is passed.

Net production change vs main is a single 20-line backfill block in
executeTurn. The e2e mock's updateSession now models the service's
title semantics (explicit title wins, existing title preserved,
untitled rows derive from prompt), and the durability e2e asserts the
raw row stays untitled until first prompt while history hydration
infers a display title from the transcript.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-08 12:30:30 -07:00
Saoud Rizwan 62a6b5a0b2 fix(core): preserve queued prompts across user-initiated aborts (#13090)
Pressing stop while prompts were queued silently destroyed them:
abort() called clearAborted(), which emptied the pending prompt queue
with no way to recover the typed input. The prompts vanished from the
UI, were never sent, and left no trace in session artifacts.

Aborting now only stops the in-flight turn. Queued prompts stay in the
queue and drain once the abort settles, matching the drain behavior
that already existed for self-aborted turns (loop detector / mistake
limit). The thrown-abort path (completeAbortedInteractiveTurn) now
schedules the same drain that runTurn schedules for turns resolving
with an aborted finish.
2026-08-08 12:20:08 -07:00
Saoud Rizwan 0caf617b50 fix(core): keep a hung MCP server from taking down session creation (#13086)
* fix(core): keep a hung MCP server from taking down session creation

A stdio MCP server that never finishes initializing used to hold its
connect open for the full DEFAULT_MCP_CONNECT_TIMEOUT_MS (doubled across
the newline/framed attempts). MCP tool discovery runs on the
session.create critical path, so that wait blew past the 30s hub command
timeout and the CLI tore the whole interactive session down instead of
just skipping the bad server.

- Bound MCP tool loading during session build with a startup budget that
  is safely under the hub command timeout. Servers that connect in time
  contribute their tools; slower/hung servers are skipped for the session
  (their error still surfaces via the MCP manager) instead of failing
  session creation. Budget is overridable via CLINE_MCP_STARTUP_BUDGET_MS
  for tests.
- Add StdioMcpClient.close() (and optional McpServerClient.close) that
  marks the client disposed so an in-flight connect() aborts its retry
  loop instead of respawning the framed fallback.
- Dispose the manager by closing clients up front, outside the per-server
  operation locks, so a server hung in initialize can no longer stall
  teardown for the full connect budget.

Adds regression tests covering both the non-blocking build and prompt
disposal while a client is hung in connect().

* refactor(core): simplify hung-MCP-server fix to a startup budget

Replace the bespoke per-server race/tracking in loadConfiguredMcpTools
with a small withStartupBudget() wrapper around the existing
Promise.allSettled: a server that exceeds the budget becomes a normal
rejection that the existing loop already logs and skips. The connect
budget, MCP settings display (initialize timeout 30s), and the rest of
the loader are left untouched.

The client close()/manager.dispose() cleanup is kept minimal: it is what
lets teardown abort a still-in-flight connect instead of blocking on the
per-server lock (and clears the pending request timer).

* fix(mcp): cap the default initialize budget at 3s to protect session creation

Supersedes the startup-budget approach on this branch with the simple
constant fix.

MCP initialize runs on the session.create critical path, which the hub
caps at 30s, and connect() can spend the budget twice (newline then
Content-Length framing). The 30s default from #13067 meant a server that
never initializes held session.create for up to 60s, so the hub RPC
timed out and the CLI tore the whole session down and exited.

Return to the pre-#13067 shape with a bigger probe: 3s instead of 1.5s.
That still covers the ~2s starters the old probe killed (#13035) and
keeps the worst case at ~6s per server, far under the hub deadline.
Genuinely slow starters (JVM-based servers like Oracle SQLcl) now need
an explicit timeout in cline_mcp_settings.json, which continues to
override the default in either direction.

Tests: update the slow-start regression tests to the new policy (2s
connects by default, 4s connects with a configured timeout), refresh the
displayed initialize-timeout assertions, and add an invariant test that
keeps the doubled default well under HUB_DEFAULT_COMMAND_TIMEOUT_MS so
the budget cannot silently creep past the session deadline again.
2026-08-08 12:18:11 -07:00
Saoud Rizwan 930575991d fix(desktop): stop opening a session from replacing the remembered model (#13091)
* fix(desktop): stop opening a session from replacing the remembered model

The composer's ModelSelector mirrored every provider/model prop change
into the remembered last selection (localStorage), which seeds new
sessions via getInitialChatConfig(). Opening an existing session drives
those props to that session's config, so merely viewing an old session
silently replaced the user's explicitly picked default model.

The remembered selection is now written only from the explicit picker
handlers (provider select and model select). Passive prop changes, such
as opening a session, no longer touch it.

* fix(desktop): re-seed remembered provider/model on chat reset

reset() kept the previous config's provider/model and only cleared the
session ID, so a chat pane that had hydrated a historical session could
carry that session's model into the next chat. Re-seed provider/model
(and apiKey when the provider changes) from the remembered defaults --
the same source a freshly mounted thread uses -- so reset and remount
behave identically.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-08 12:10:39 -07:00
Saoud Rizwan e40d7d44ae fix(desktop): stop treating leftover plugin install dirs as installed (#13095)
* fix(desktop): stop treating leftover plugin install dirs as installed

isOfficialPluginInstalled() only checked that the marketplace install
directory existed. A failed or interrupted install can leave that
directory behind with no plugin inside, and the next install attempt
then short-circuited with a fake 'already installed' success: the
marketplace button flipped to Uninstall with no error while nothing
actually worked, and the installed-entries listing kept reporting the
broken entry as installed.

The check now requires a loadable plugin module inside the directory
(via discoverPluginModulePaths) before reporting the entry as
installed, so partial directories fall through to a real install
attempt whose outcome is surfaced to the UI.

* fix(desktop): reclaim leftover partial plugin install dirs with --force
2026-08-08 12:06:35 -07:00
Saoud Rizwan e973ce4f33 fix(cli): make queued message text readable on light theme TUI (#13098)
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-08 12:00:50 -07:00
Saoud Rizwan 4f25692d70 fix(desktop): canonicalize diff panel paths against the session cwd (#13092)
* fix(desktop): canonicalize diff panel paths against the session cwd

Tool calls address the same file inconsistently across a session: one
edit uses a workspace-relative path (journal.txt), a later one the
absolute path (/tmp/ws/journal.txt). mergeToolDiffs keyed entries by the
raw string, so the same file was listed twice in the diff panel with
split +/- counts and inconsistent naming, most visibly after git was
initialized mid-session and the model switched to absolute paths.

Diff paths are now canonicalized against the session cwd before
merging: entries for the same file collapse into one, files inside the
cwd display as workspace-relative paths, and files outside it display
their resolved path. Without a cwd the previous raw-key behavior is
kept.

* style: collapse editorReplaceEvent signature per biome format

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): collapse dot segments and keep root cwd in diff path keys

* fix(desktop): compare Windows diff path keys case-insensitively

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-08 11:55:58 -07:00
Saoud Rizwan b0bba2e5d6 fix(vscode): hide View Changes on completion rows until there are changes to show (#13096)
* fix(vscode): hide View Changes on completion rows until there are changes to show

The button previously always rendered on the latest completion row, faded
and disabled when the count check came back 0 - which covers both 'nothing
changed since your last message' and 'no checkpoint to compare against'
(non-git workspace, repo with no commits, comparison failure). A dead
button with a misleading tooltip in the non-git case is worse than no
button: now the row renders nothing until the host confirms there are
actual changes, and the button is always enabled when shown.

* fix(vscode): reset View Changes state when showViewChanges toggles

Greptile review: a stale positive hasChanges from a previous evaluation
could flash the button before the host confirms the new comparison when
showViewChanges flips false and back true on the same row. Reset to
'still checking' whenever the effect re-runs.
2026-08-08 11:41:41 -07:00
Saoud Rizwan a6e2c0c431 fix(core): never run a foreign compiled plugin-sandbox bootstrap for a source host (#13094)
* fix(core): never run a foreign compiled plugin-sandbox bootstrap for a source host

When @cline/core runs from source (e.g. the desktop hub daemon in dev)
with CLINE_WRAPPER_PATH set, resolveBootstrap() picked the compiled
plugin-sandbox-bootstrap.js from a separately installed CLI platform
package (such as a published version sitting in the package-manager
cache) before falling back to the source bootstrap. That bootstrap
resolves modules against the other installation's layout, so every
plugin failed to load with "Cannot find module '@cline/core'" - and
the settings pipeline swallowed the failure, leaving Settings > Tools
showing "No plugin tools found" and plugins showing no contributions
even though the same plugins loaded fine in chat sessions.

Bootstrap selection now prefers, in order: a compiled bootstrap next to
this module (always matches the host build), the source bootstrap when
the host runs from source, and only then wrapper/executable-derived
bootstraps - which remain the path for compiled binaries where
import.meta points inside the bunfs bundle.

* chore(core): restore untouched settings-service formatting
2026-08-08 11:23:25 -07:00
Saoud Rizwan d011d049a1 fix(core): keep session context durable across aborts and hub restarts (#13078)
* fix(core): keep session context durable across aborts and hub restarts

Users on slow self-hosted endpoints reported sessions losing their entire
conversation after cancelling a long-running request: the TUI still showed
the transcript, but the next turn greeted them like a brand-new session.

Root cause is a stack of two failures:
1. The hub daemon exits on any unhandled rejection that is not an
   AgentRuntimeAbortError, so a floating abort-family rejection from a
   cancelled provider stream kills every resident session.
2. When the CLI recovers the missing session it rebuilds from the persisted
   messages file - but aborting a turn never flushed the transcript, and
   lazy session persistence (SDK 0.0.70) kept seeded history (mode-switch
   restarts, forks, previous recoveries) memory-only until the first
   completed turn. Recovery then seeds an empty session: silent context wipe.

Fixes:
- completeAbortedInteractiveTurn now flushes the transcript to disk, so an
  aborted exchange survives a hub restart.
- Sessions started with initialMessages persist them (and any compaction
  sidecar) immediately; brand-new empty sessions stay lazy, so closing an
  unused runtime still leaves no empty history entry.
- The hub daemon ignores abort-family unhandled rejections (DOMException
  AbortError, Node ABORT_ERR) the same way it already ignores
  AgentRuntimeAbortError, instead of exiting with every session resident.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): write seeded history atomically with session materialization

Greptile review flagged a residual crash window in the seeded-session
persistence: ensureSessionPersisted created the session row (with an empty
messages file) and only then called persistSessionMessages, so a crash
between the two left a discoverable session whose seeded history was gone.

Close the window by threading initialMessages/systemPrompt through
createRootSessionWithArtifacts: the messages artifact is now written with
the seeded transcript before the session row is committed, so every crash
point leaves either nothing discoverable or complete data. The follow-up
persistSessionMessages call at session start is gone; the seed travels
inside session materialization.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Revert "fix(core): write seeded history atomically with session materialization"

This reverts commit 5a7e0b37f1.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-07 21:44:40 -07:00
Saoud Rizwan e09afced69 fix(core/hub): report queued-turn failures as run.failed (#13074)
Turns drained from the pending-prompt queue resolve their errored
AgentResult inside PendingPromptsController.drain(), which discards it,
and the legacy 'error' agent event had no projection in the hub's
session-event projector — so a failed queued turn never produced any
terminal hub event. Interactive clients (e.g. the desktop app) hung on
'Thinking...' with no error shown.

The projector now publishes run.failed (with the error text and a core
session snapshot) for non-recoverable lead-agent error events, but only
when no RPC-driven turn is awaiting sessionHost.runTurn for that
session — the awaiting run.start handler already publishes the
authoritative terminal event, so this avoids double-reporting a turn
that resolves through both paths.
2026-08-07 20:52:01 -07:00
Saoud Rizwan 3f83be51a2 feat(vscode): fade View Changes button until changes since last message are confirmed (#13076) 2026-08-07 20:16:15 -07:00
Saoud Rizwan 1efe5577e1 fix(core/cli): drain queued prompts after self-aborted turns and surface the stop (#13061)
* fix(core/cli): drain queued prompts after self-aborted turns and surface the stop

When a run ends with finishReason "aborted" without a user abort request
(loop detector hard escalation or the consecutive-mistake safety stop),
runTurn skipped the pending-prompt drain, stranding user-queued messages
forever, and the CLI rendered nothing - the task appeared to silently
stop with queued messages never consumed (#13030).

- core: schedule the drain after every completed turn, including
  aborted/error finishes. User-initiated aborts are unaffected because
  abortSession() already clears the queue, and drain() stops after one
  failed send so an erroring provider cannot spin the queue.
- cli: when a turn comes back aborted without the user having requested
  an abort, append a "Task stopped before completion." status entry
  instead of ending the turn silently.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): hold queued prompts on error finishes instead of consuming them

Addresses the Greptile P1 review on #13061: a drained prompt whose turn
resolved with finishReason "error" returned normally, so the
exception-only requeue path treated the send as successful - the failed
prompt was consumed and draining continued firing the remaining queue
into a failing provider.

- drain() now stops the chain when a drained send resolves with an
  error finish. The errored entry itself is not requeued (its turn ran:
  the prompt is in the conversation and the error is surfaced), but the
  rest of the queue is held.
- runTurn() no longer schedules a drain after "error" finishes (the
  skip is removed only for "aborted", which is the #13030 fix).
  Held prompts still drain via the existing enqueue/update/delete
  triggers or the next successful turn.
- Two new unit tests cover both layers.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert(cli): drop the 'Task stopped before completion' status line

Keep the change scoped to the queue-drain fix in @cline/core. The CLI
no longer prints a notice for non-user-initiated aborted finishes;
apps/cli is back to parity with main. When messages are queued, the
drain itself makes the stop visible (the queued message runs); richer
stop-reason surfacing can be a follow-up.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-07 19:27:47 -07:00
Saoud Rizwan 9cbc24d6e5 Restore "View Changes" on completion rows using SDK checkpoints (#13072)
* fix(core): read untracked-at-snapshot files from stash third parent in checkpoint diff

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(vscode): restore View Changes button on completion rows via SDK checkpoint diff

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): integrate View Changes as a footer inside the completion card

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style(vscode): place the View Changes button inside the completion card

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-07 18:11:25 -07:00
Saoud Rizwan 551286d33b fix(cli): preserve binary MCP payloads in expanded TUI tool output (#13071)
* fix(cli): preserve binary MCP payloads in expanded TUI tool output

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* simplify to minimal payload-preserving fix

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-07 18:10:28 -07:00
Saoud Rizwan 361ac90978 fix(llms): route LiteLLM through Chat Completions instead of the Responses API (#13053)
The litellm builtin spec pinned protocol: "openai-responses", so every
request went to POST {baseUrl}/responses. Self-hosted LiteLLM proxies
commonly implement only /chat/completions, so all prompts failed with
404 Not Found on the SDK path (CLI, and now the Next extension bundle).

Drop the override so litellm inherits the openai-compatible family
default (openai-chat -> /chat/completions), matching every sibling
openai-compatible builtin and the Legacy extension behavior.

Fixes #13003, fixes #10781
2026-08-08 02:06:46 +02:00
Saoud Rizwan 4eb7402334 fix(cli): render MCP tool result text instead of escaped JSON in TUI (#13066)
* fix(cli): render MCP tool result text instead of escaped JSON in TUI

MCP tools return {content: [{type: "text", text}]} which
extractFullOutputText JSON-stringified, escaping newlines into one giant
line that word-wrapped across the whole terminal and never triggered the
line-based collapse. Extract the text parts with real newlines so the
existing collapse works.

Fixes #13038

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): keep placeholders for non-text blocks in mixed MCP results

Addresses Greptile review on #13066: text-only filtering silently
dropped image/resource/audio blocks from mixed MCP content. Render them
as [type] placeholders instead.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): surface non-text MCP block metadata in TUI output

Extract embedded resource text, and include resource/resource_link URIs
and image/audio mime types in placeholders so expanded mixed MCP
results keep identifying metadata.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-07 17:05:22 -07:00
Saoud Rizwan ffb61a865f fix(mcp): give unconfigured stdio servers a 30s initialize budget (#13067)
The stdio MCP client gave servers without a configured `timeout` only
1.5 seconds to answer initialize before killing the process, so
slow-starting servers (e.g. Oracle SQLcl's JVM-based `sql -mcp`) could
never load and were silently skipped at session start.

Raise the default connect budget to 30s, in line with the startup
budget other MCP clients allow. A configured `timeout` still overrides
it in either direction, dead commands still fail fast through the spawn
error/exit path, and the newline -> Content-Length framing fallback is
unchanged.

Fixes #13035

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-07 16:49:28 -07:00
Bee d3616b96ae feat(desktop): route /team prompts through core runtime (#12372)
* feat(desktop): route /team prompts through core runtime

Rewrite desktop `/team` commands as structured user command blocks before sending them to the core runtime. Validate task input and respect the globally disabled Teams tool setting.

Remove legacy agent spawn and team enablement flags from session configuration, and add coverage for prompt rewriting and disabled-tool behavior.

* fix(desktop): preserve team tool defaults

* fix(desktop): display queued /team prompts as their slash form

Queued prompts are stored in their runtime form, so a queued /team
command showed its raw <user_command> envelope in the prompt queue chip
and edit textarea. Fold queue items through formatDisplayUserInput for
display; saving an edit re-resolves the slash form through the sidecar,
so the round trip is lossless.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(hub): align builtin tool catalog flags with the desktop sidecar

The desktop sidecar pins enableSpawnAgent/enableAgentTeams when listing
the builtin tool catalog; the hub's parallel listing did not, so the two
would drift if the preset defaults ever change. Pin the same flags in
the hub and cross-reference the two call sites.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): drop inert enableSpawn/enableTeams config leftovers

buildCoreSessionConfig no longer reads these keys, so remove the dead
schema fields, default-config initializers, and chat-test payload
entries. The chat-session regression test still sends them on purpose
to prove legacy flags cannot override the runtime's tool presets.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): reject /team when the mode's tool preset disables teams

The /team guard only checked the global disabled-tools setting, but the
runtime resolves tool availability from the mode's preset, so a preset
without team tools (yolo) would still send the model a spawn-a-team
instruction it cannot act on. Resolve the teams catalog entry for the
session's mode and reject /team when it is unavailable, mirroring the
runtime's own availability logic.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-07 16:45:49 -07:00
Bee 98e5458b09 feat(hub): centralize plugin settings and contributions (#12942)
* fix(desktop): plugin package names

* feat(hub): centralize plugin settings and contributions

* fix(hub): address plugin settings review feedback

* fix(settings): make plugin snapshots host-aware

* fix(core): make host plugin toggles atomic

---------

Co-authored-by: cline-cloud[bot] <cline-cloud[bot]@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-07 16:44:02 -07:00
Saoud Rizwan 031b8d94e1 fix(core): surface OAuth authorization for SSE MCP servers on 401 (#13050)
* fix(core): surface OAuth authorization for SSE MCP servers on 401

A 401 from an SSE MCP server never persisted authorizationRequired: the
fetch-boundary UnauthorizedError was consumed by EventSource and re-thrown
as a status-less SseError, so the instanceof check routed it to
markConnectionError and hosts never offered the OAuth connect action.

Give the SSE stream request a raw fetch so a 401 fails the connection with
the SDK's typed SseError(401), and recognize 401s across transports with a
single isMcpUnauthorizedError predicate at every detection site.

* style(core): apply biome formatting to MCP oauth changes
2026-08-07 16:42:43 -07:00
Saoud Rizwan e5bcba8ef9 fix(vscode): settle the turn phase when a mode switch aborts a running turn (#13063)
Toggling Plan/Act while a turn was streaming or waiting on a tool approval
aborted the turn but left the TurnStateTracker on its last live phase: the
aborted session's done event is fenced off as stale once the rebuild
unsubscribes it, so nothing ever settled the phase. The webview then kept
rendering that phase forever - an eternal Thinking spinner with the input
disabled (aborted while streaming), or dead Approve/Run Command buttons wired
to an approval that clearPending had already denied (aborted while awaiting
approval). Users experienced this as 'switched to act mode and nothing
happened / it never wrote the files'.

Mirror cancelTask: after aborting the turn for the mode change, append a
resume_task ask row and set the phase to resumable, so the footer offers
Resume Task with the input enabled in the new mode.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-07 16:41:13 -07:00
Saoud Rizwan 83182c0d96 fix(llms): retry mid-stream network interruptions before any model output (#13052)
* fix(llms): retry mid-stream network interruptions before any model output

* fix(llms): scale network retry backoff by network retry count, not shared attempt number
2026-08-07 16:36:27 -07:00
Saoud Rizwan 40ebd09dfa desktop: native-feel polish, render-path performance, and transition fixes (#13028)
* desktop: native-feel polish and render-path performance fixes

- Suppress the WebView browser context menu on app chrome (keep it for
  editable fields and active text selections)
- Make UI chrome unselectable app-wide; opt chat messages, markdown,
  code, diffs, and error banners back into text selection
- Contain overscroll so inner scrollers don't rubber-band the window
- Lazy-load Settings/Sessions/Onboarding/Diff views out of the entry chunk
- Memoize ChatInputBar and AgentHeader; stabilize their props in the chat
  pane so stream flushes only re-render the affected message bubble
- Stop refocusing the composer textarea on every keystroke (caret flicker)
- Cache slash commands across menu opens (stale-while-revalidate)
- Avoid rebuilding reversed message arrays and ask-question JSX per render
- Drop core info/debug console logging on the streaming hot path behind a
  cline:debug-logs opt-in; remove leftover [webview:delete] debug logs
- SearchCombobox (provider/model picker): Escape closes and restores focus
- Remove unused @vercel/analytics, recharts, embla-carousel deps and the
  unused chart/carousel UI components

* desktop: surface failed-turn errors instead of leaving the chat blank

On a failed run the runtime reports its error string in result.text.
The webview rendered that as an assistant bubble, which the canonical
history rehydration then wiped (the failed turn is never persisted),
so provider errors like a retired model id left the user staring at a
silently empty chat. Route failed-turn text to a persistent error-role
message added after rehydration instead.

* desktop: fade the welcome/conversation swap instead of hard-cutting

Sending the first message replaced the hero layout with the message
grid in a single commit, which read as a white flash. A 180ms enter
animation now plays when either side becomes visible; disabled under
prefers-reduced-motion.

* desktop: render new-chat panes instantly from the last catalog load

Clicking + remounts ChatThreadPane, which refused to render until the
provider catalog (a large fetch) and workspace list resolved again —
about a second of blank pane plus boot spinner on every new chat.
Seed remounts from a module-level snapshot of the last successful
load; the mount effect still refreshes both in the background.

* desktop: invalidate the provider-catalog snapshot with the cache

Seeding remounted chat panes from the last catalog load left a window
where a pane created right after a credential change could act on the
old keys. The snapshot now lives in the catalog module and is dropped
by invalidateProviderCatalogCache(), so credential edits force the
next remount to wait for fresh data.
2026-08-07 16:18:47 -07:00
Saoud Rizwan 7bc18f7e15 Bring back a copy button on turn-final response rows with a subtle header (#13051)
* Add subtle response header with copy button to completion and plan rows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add changeset for response header copy button

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename turn-final headers to Completed and Plan

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-07 16:12:43 -07:00
Bee fad8006730 feat(hub): add execution context for scheduled run reports (#12718)
* feat(hub): add execution context for scheduled run reports

Add human-readable headers, schedule metadata, durations, and lifecycle error context to cron run reports. Resolve file-based definitions to real paths while clearly identifying Hub-managed schedules stored in cron.db.

* fix cron report formatting edge cases

* Escape schedule titles in reports

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-07 15:57:33 -07:00
Saoud Rizwan 28f35a2ec8 fix(cli): harden tool input/output formatters against malformed payloads (#13048)
* fix(cli): harden tool input/output formatters against malformed payloads

Tool inputs cross the model/tool boundary and may not match their
TypeScript annotations (e.g. run_commands with { command: null }).
truncate() called str.replace() on such values, crashing the TUI with
'.replace is not a function' and making persisted sessions containing
the payload non-resumable, since hydration replays the same input
through formatToolInput().

Normalize untrusted values at the formatting boundary: truncate() now
accepts unknown and safely stringifies null/undefined/objects (including
circular structures and throwing toJSON), formatStructuredCommand no
longer returns non-string commands verbatim, and fetch_web_content
request summaries tolerate malformed entries.

Fixes #13036

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): keep valid empty-string args in structured command summaries

Greptile review: filtering normalized args by truthiness also dropped
genuine empty-string argv entries, so summaries could show a different
argument list than the one executed. Filter only nullish entries before
normalization instead.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-07 15:53:01 -07:00
Saoud Rizwan 1e24807d1a fix(vscode): fall back to session cwd or Desktop for @-mention file search in empty windows (#12982)
* fix(vscode): fall back to session cwd/Desktop for @-mention search in empty windows

* fix(vscode): use the shared chat workspace as the no-folder fallback root
2026-08-07 15:38:43 -07:00
Saoud Rizwan a5f90e0d53 fix(core): pick up checkpoints when git is initialized mid-session (#13026)
ensureGitRepository cached a negative probe for the lifetime of the hook
instance, so a session started in a non-git folder never got checkpoints
even after the user ran git init. Cache only the positive answer and
re-probe otherwise; the probe runs at most once per user turn.
2026-08-07 15:25:11 -07:00
Saoud Rizwan adabfc6bd5 fix(desktop): treat signed-out state as a typed result instead of a command error (#12976)
* fix(desktop): treat signed-out state as a typed result instead of a command error

* fix(desktop): sign out when the organization balance fetch reports the typed signed-out result

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: retrigger checks after runner outage

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-07 15:06:45 -07:00
Saoud Rizwan 397d6f344f fix(desktop): resolve startup script-load SyntaxError and attribute webview errors to their source URL (#12974)
* fix(desktop): remove Vercel Analytics injection that breaks packaged webview startup

* fix(desktop): attribute webview uncaught errors to their source URL

* chore: drop unrelated claude-dev version bump from lockfile

* chore: retrigger checks after runner outage
2026-08-07 15:02:36 -07:00
Bee 8ca3068b73 refactor(desktop): update team tools component (#13047)
* refactor(desktop): update team tools component

* fix(desktop): address team tool review feedback
2026-08-07 22:58:06 +02:00
John Choi 6e6befdb65 fix(ui): remove nested tool output scrolling (#13043)
* fix(ui): remove nested tool output scrolling

* fix(desktop): avoid nested tool output scrolling

* chore(desktop): remove obsolete scroll utility

* fix(desktop): preserve multiline tool details
2026-08-07 13:44:23 -07:00
Haley Park 71536e55aa refactor(ui): introduce Cline-owned semantic color system (#12941)
* refactor(ui): introduce Cline-owned semantic color system

* refactor(desktop): adopt shared semantic theme roles

* refactor(ui): set 15px root and recalibrate xs/sm type scale

Scale rem steps so xs/sm stay 12/13px visually, and slightly lift dark-mode neutral-4.

* refactor(ui): align SearchCombobox with package type and hover tokens

Use host-safe cline-ui utilities and keep option font inheritance from CSS.

* fix(ui): use standard stroke-2 utility on approval spinner

* refactor(desktop): modernize shared UI primitives for Tailwind v4

Replace legacy arbitrary/has selectors with current utility syntax.

* refactor(desktop): bump chat chrome typography to text-sm

Keep composer controls and pickers on the shared sm type step.

* refactor(desktop): use max-w-344 for page frame content width

* chore(desktop): disable Next.js dev indicators

* chore: ignore desktop-app Cursor settings

* docs(pr): add before/after screenshots for #12941

* chore: retrigger checks

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-07 10:15:18 -07:00
Saoud Rizwan 7348ba1847 chore(desktop): release v0.0.10 2026-08-06 23:45:56 -07:00
Bee 3e96fc6112 feat(cli): add mcp uninstall command (#12985)
* feat(cli): add mcp uninstall command

* unit test wiring

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-06 19:40:45 -07:00
Saoud Rizwan d84d09c543 desktop: fix silent turn failures, message duplication, and stuck composer; add first-run setup guidance (#12984)
* desktop: fix silent turn failures, message duplication, and stuck composer; add first-run setup guidance

Findings from two full computer-use UX audits of the desktop app:

- Surface failed turns in the transcript: queued turns (incl. the first
  prompt of a fresh session) only signal errors via chat_done, which the
  UI previously ignored - sending a message with no credentials failed
  in complete silence. Failed turns now show an error message enriched
  with the latest core error log and a pointer to Settings -> Models.
- Fix duplicated user messages: a live send's optimistic user message
  was materialized a second time by the runtime's queued-prompt-start
  event.
- Fix composer stuck on 'Agent is working...': drop prompts from the
  local queue snapshot when they start, emit a fresh queue snapshot from
  the sidecar on pending_prompt_submitted, and double-check the server
  queue on turn completion.
- Add a 'Connect a model' notice on the welcome screen when no provider
  has credentials, with actions to reopen onboarding at the connect step
  or jump to model settings; it reacts live to credential changes.
- Add 'Get an API key' links for popular providers in onboarding and
  Settings -> Models (the catalog docUrl is never populated), and link
  the Cline dashboard from the Cline API key form.
- Explain what Cline is on the onboarding welcome step.
- Make the stop button visible (was 8px with no padding) and support
  Esc to stop; add Cmd/Ctrl+N (new session) and Cmd/Ctrl+, (settings).
- Remove leftover [webview:delete] console.error debug logging that
  surfaced an error badge after deleting a session.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: remove remaining delete debug logging in session history hook

The sidebar right-click delete path had the same leftover [webview:delete]
console.error instrumentation, which made the Next dev-mode issues badge
appear after every deletion.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: fix Biome a11y error in WelcomeSetupNotice

biome's lint/a11y/useSemanticElements errors on role="status" divs;
use the semantic <output> element (implicit status role) instead. This
was failing the repo's 'bun run lint'.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: count structured-config and keyless providers as connected

The welcome setup notice previously only recognized apiKey/OAuth
credentials, so users running Bedrock/Vertex (structured configValues)
or a deliberately enabled keyless local endpoint (e.g. Ollama) were
nagged to connect a model they already use. isProviderConnected now
also counts an enabled provider whose required config fields are all
filled, or an enabled provider that has no API-key field at all.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: keep re-key eligible when chat_done lands in the same batch as its prompt start

When a turn fails fast, chat_queued_prompt_start and chat_done can be
dispatched in one React batch. Clearing the outstanding-optimistic-
bubble registry synchronously in the chat_done handler ran before the
re-key updater enqueued by the prompt-start event, so the optimistic
bubble was appended a second time instead of re-keyed. Clear the
registry inside a state updater so it executes in event order after
the re-key. Caught by the queued-turn-failure regression test.

* desktop: make the queued-prompt re-key updater idempotent under StrictMode

React StrictMode double-invokes state updaters in dev. The
chat_queued_prompt_start re-key updater consumed the optimistic
bubble's id from outstandingOptimisticUserIdsRef on its first run, so
the second run against the same prev found no eligible candidate and
appended the same user message a second time (and, without a promptId,
makeId() minted a different id per invocation). Hoist the message id
out of the updater and remember which optimistic bubble each queued
message id re-keyed so a re-run reaches the identical result. The memo
resets alongside the outstanding set (error state, reset, hydration).

Root-caused with runtime instrumentation: the duplicate only appeared
on turns that exercised the queue-drain re-key path, and hydration
later collapsed it to one message because the duplicate never existed
in persisted state.

* desktop: preserve failure messages across post-send canonical hydration

Persisted history never contains UI-only error bubbles, so the two
post-send read_session_messages replacements in sendPrompt wiped the
failure explanation appended from chat_done ~40ms after it rendered
(confirmed with runtime instrumentation). Re-append the active
session's error messages after the canonical history. Includes a
regression test reproducing the chat_done-error-then-RPC-resolution
race.

* desktop: don't let an optional API-key field veto a connected provider

Greptile P1 follow-up: Bedrock's catalog entry carries an optional
apiKey field ('Optional Bedrock bearer token') alongside IAM/profile
authentication, and keyless local endpoints can also surface one — so
treating the mere presence of an apiKey field as proof of disconnection
kept nagging configured users. An enabled provider (the user
deliberately persisted settings for it) now counts as connected unless
a required config field is unmet; auth may legitimately live outside
the catalog (IAM, env vars, local endpoints). Brand-new users have no
enabled providers, so the first-run notice still shows for them.

* desktop: tighten credential-error guidance and stop re-pinning stale failure bubbles

* desktop: invalidate the shared provider catalog after settings OAuth login

Greptile P1 follow-up: runOAuthProviderLogin only updated the settings
view's local provider state, so the shared catalog cache and its
invalidation subscribers (the composer selector and the welcome
screen's 'Connect a model' notice) kept reporting the provider as
disconnected until an unrelated invalidation or a pane remount. Notify
the shared cache on successful OAuth login, like the account view and
the API-key save path already do.

* desktop: clear the remembered core error on turn end, reset, and hydration

Greptile flagged that turn-start events are the only thing clearing
lastCoreErrorBySessionRef, and websocket events are not replayed: a
transport interruption that drops a turn's start event lets a later
detail-less failure resurrect an earlier turn's error. The remembered
error belongs to exactly one turn, so clear it whenever a turn ends
(chat_done, any reason) as well as on reset() and history hydration.
Regression test covers the dropped-start-event sequence.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-06 19:25:42 -07:00
Bee 6f7f817d63 feat(core): support pre-registered OAuth clients for remote MCP (#12983)
* feat(core): support pre-registered OAuth clients for remote MCP

* fix(core): invalidate tokens when OAuth client changes

* fix(core): preserve compatible MCP OAuth sessions

* fix tests

* feat(desktop): wire mcp oauth in desktop

* UI update

* fix(core): reject stale mcp oauth callbacks

* fix(mcp): handle invalid settings and preserve state

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-06 18:58:52 -07:00
Bee c18d9478ba feat(cli): use saved provider settings for schedules (#10667)
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-08-06 17:34:40 -07:00
Ara 574b8eb45e fix(llms): use configured fetch for Vertex ADC refreshes (#12981) (#12991) 2026-08-06 10:07:06 -07:00
Saoud Rizwan 81cce3d70e chore(vscode): prepare 4.1.6 release 2026-08-06 00:47:54 -07:00
Saoud Rizwan e1352fa709 chore(cli): release v3.0.51 2026-08-06 00:28:37 -07:00
Saoud Rizwan 394fb04518 chore(sdk): release v0.0.71 2026-08-06 00:14:21 -07:00
Saoud Rizwan f1aebbfd5a feat(llms): surface meta/muse-spark-1.2-contributor for the Cline provider (#12995)
* chore(llms): regenerate model catalog from models.dev

* feat(llms): surface meta/muse-spark-1.2-contributor for the Cline provider

* test(llms): guard Vercel-only Cline model allowlist
2026-08-06 00:04:16 -07:00
Saoud Rizwan 543dd0d818 fix(telemetry): attribute agent.run sdk.error events to the active model (#12972)
* fix(telemetry): attribute agent.run sdk.error events to the active model

* fix(telemetry): strip undefined values from sdk.error properties
2026-08-05 17:51:06 -07:00
Saoud Rizwan 1f2cbbeb9f chore(vscode): prepare 4.1.5 release 2026-08-05 14:04:04 -07:00
Saoud Rizwan b1a89156d6 feat(vscode): explain when a free model promotion ends (#12970)
* feat(vscode): explain when a free model promotion ends

Once a free promotion ends, the cline-free/ model is removed from the
catalog and the backend answers 'model not found' to requests against it.
The CLI has shown a dedicated 'Free model promotion ended' banner for this
since #12593; the extension instead rewrote the answer into generic
model-not-found guidance with no model-picker offramp.

Detect the case in the host where the active model id is known
(reshapeErrorForWebview, fed by a new MessageTranslatorState model-id
source), stamp the payload with a cline_free_promotion_ended code, and
render a dedicated card in the webview with a button into the model
picker. Classification is gated on the cline-free/ prefix so ordinary
model-not-found errors keep their generic path, and it runs before the
auth branch since the 404 status falls inside the generic auth range.

* fix(vscode): prefer the live task model over session-start metadata

A mid-task model-only switch updates the running session's model in place
(updateActiveSessionModel) and refreshes the task API shim, but never
touches the session's startConfig/manifest. Preferring the session-start
snapshot could therefore misclassify after such a switch: a genuine
retired-model 404 would miss the promotion-ended card, and the reverse
switch could show it for the wrong model. Provider switches restart the
session, so both sources agree there; the shim starts as "unknown"
(filtered out), so fresh sessions still resolve through start metadata.
2026-08-05 14:00:09 -07:00
Bee 1d7d9ce5e2 feat(llms): add portable reasoning resolution for AI SDK providers (#12946)
* feat(llms): add portable reasoning resolution for AI SDK providers

Introduce resolvePortableReasoning to map gateway reasoning requests
(effort levels, enabled/disabled flags) to the AI SDK's top-level
reasoning setting, applying it in buildAiSdkStreamConfig for supported
providers including Ollama.

- Defer exact token budgets to provider-specific options
- Omit reasoning when the caller expresses no explicit intent
- Replace manual provider-specific thinking overrides (e.g. Anthropic
  budget clamping, Moonshot/OpenAI-compatible toggles) with the
  portable reasoning path where applicable
- Add tests covering effort mapping, budget passthrough, and provider
  stream config integration

* fix(llms): prioritize explicit reasoning disable

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-05 12:17:49 -07:00
Mikołaj Kondratek 78b7c3d8ac fix(desktop): stop rendering the first chat message twice (#12779)
* fix(desktop): dedupe chat_queued_prompt_start emitted for the same prompt

PendingPromptService.drain() emits a pending_prompts snapshot (head
removed) and a pending_prompt_submitted event back-to-back for the same
prompt. The sidecar translated both into chat_queued_prompt_start, so
the webview rendered the user's message twice until the chat was
re-hydrated from history. Track the last announced prompt id per live
session and emit the start chunk once.

* fix(desktop): re-key optimistic user bubble when the runtime queues the prompt

The send path renders an optimistic user bubble for prompts dispatched
while the session is idle, keyed by a random id. When the runtime
routes that prompt through its pending queue (e.g. during session
startup), the queued-prompt-start event appended a second bubble under
queued_user_<promptId> — the same message rendered twice until the
chat was re-hydrated from history. Re-key the trailing optimistic
bubble to the event's id instead of appending.

* fix(desktop): re-key only outstanding optimistic bubbles on queued prompt start

Review follow-up: matching by content alone could swallow a new queued
prompt that repeats the text of a message left at the transcript tail
by an earlier cancelled/failed turn. Track in-flight optimistic bubble
ids explicitly (registered on optimistic append; cleared on re-key,
turn end, error, and history hydration) and only re-key those.
2026-08-05 14:49:05 +02:00
Saoud Rizwan d626cfb0b5 chore(vscode): prepare 4.1.4 release 2026-08-05 03:03:51 -07:00
Saoud Rizwan e14f354c59 chore(desktop): release v0.0.9 2026-08-05 02:29:46 -07:00
Saoud Rizwan 41ba332f0a chore(cli): release v3.0.50 2026-08-05 02:16:56 -07:00
Saoud Rizwan 6997fae815 chore(sdk): release v0.0.70 2026-08-05 01:59:51 -07:00
Saoud Rizwan 5594512eb0 fix(vscode,cli): recoverable agent errors must not kill a turn that completes with a plan (#12953)
* fix(core): don't count plan-mode guard-blocked commands as model mistakes

The plan-mode command guard (#12906) rejects file-editing run_commands
calls with a tool error. The orchestrator counted that error as a failed
tool call, so a turn whose only tool call was guard-blocked fed the
MistakeTracker, which emits a recoverable "error" AgentEvent
("1 tool call(s) failed: [run_commands] ...").

Hosts render that event as a failed turn. In the VS Code extension the
turn ended in the "error" phase (Retry / Start New Task footer), the
final plan text was never retagged to plan_completion_result, and
toggling to Act therefore rebuilt the session without the auto-continue
send - the toggle appeared to do nothing and the presented plan was
never acted on. In the CLI TUI the same event flipped the footer to
idle mid-turn.

A guard rejection is deliberate session policy, not a model mistake:
the run continues and the model is expected to fold the change into
its plan. Tag the guard error with a stable marker sentence, expose
isPlanModeBlockedCommandError, and skip the failed-tool bookkeeping for
matching results so no mistake is recorded and no error event is
emitted. Repeated blocked attempts are still bounded by loop detection
and maxIterations.

* docs(core): flag plan-mode guard error string matching for typed skip channel

FIXME on isPlanModeBlockedCommandError: recognizing guard rejections by
sniffing the error text is brittle. The intended replacement is a typed
skipSource/skipCode on the tool-finished runtime event so the
orchestrator (and the VS Code approval-denial suppression) can identify
skipped tools structurally instead of via string matching.

* Revert core mistake-counting change for plan-guard blocks

A model attempting a file-editing command in plan mode is disobeying
its instructions - that IS a model mistake, and the MistakeTracker
should keep counting it (it is the brake that stops weak models from
flailing at blocked commands indefinitely). The real bug is host-side:
a recoverable mid-turn mistake must not kill a turn that afterwards
completes with a presented plan. The follow-up commit fixes that in
the hosts instead.

* fix(vscode,cli): treat recoverable agent errors as in-run notices, not turn outcomes

The MistakeTracker emits a recoverable error event for every recorded
mistake while the run continues - e.g. a plan-mode guard-blocked
run_commands call as the turn's only tool call. Both hosts treated any
error event as terminal:

- The VS Code translator cleared the pending completion retag, set
  errorSeen (turn phase "error": Retry / Start New Task footer), marked
  the turn complete, and rendered the error recovery UI. A plan turn
  that recovered from the mistake and completed cleanly therefore never
  produced plan_completion_result, so togglePlanActMode's planPresented
  check failed and switching to act mode rebuilt the session without
  the auto-continue send - the toggle appeared to do nothing.

- The CLI TUI flipped isRunning/isStreaming to idle mid-turn, so the
  footer lied about the still-running turn.

Recoverable errors are informational: the turn's outcome is decided by
how it actually ends (done/error). VS Code now logs them and keeps them
out of the chat (the tool failure is already shown inline on its tool
row, and provider-failure telemetry already ignores recoverable events
for the same reason); the CLI keeps its running state and surfaces them
only in verbose mode, as it already did for display. Genuine run
failures carry recoverable: false and keep the existing error UI.
2026-08-05 01:02:23 -07:00
Saoud Rizwan 6712d43c69 Revert "fix(llms): flatten top-level tool schema unions before sending to pro…" (#12950)
This reverts commit 21edad82a6.
2026-08-04 21:11:33 -07:00
Bee bd27d9c41b feat(desktop): session source filtering (#12943)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-04 21:01:40 -07:00
Saoud Rizwan 21edad82a6 fix(llms): flatten top-level tool schema unions before sending to providers (#12948)
Anthropic (and several providers OpenRouter fans out to) rejects any tool
whose input_schema has oneOf, allOf, or anyOf at the top level, failing the
whole request with:

  tools.N.custom.input_schema: input_schema does not support oneOf, allOf,
  or anyOf at the top level

MCP servers commonly advertise tools whose input schema is a union of object
shapes (e.g. generated from a Zod union), so one such tool bricked every
turn of the session. Merge union branch properties into a single object
schema at the provider boundary; tools still validate their real input
shapes in execute().

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 20:56:39 -07:00
Tomás Barreiro 2f58bbe4ed Add Auto Approval to ACP (#12897)
* Add Auto Approval to ACP

* Update apps/cli/src/acp/auto-approve.ts

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

* Update apps/cli/src/acp/auto-approve.test.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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-04 20:54:39 -07:00
Saoud Rizwan ddfb67515b fix(desktop): inline telemetry config into the packaged sidecar binary (#12925)
* fix(desktop): inline telemetry config into the packaged sidecar binary

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): reject non-http OTLP endpoints in the telemetry selfcheck

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 20:43:01 -07:00
Saoud Rizwan 472f9c88c5 Add plan-mode command blocklist to run_commands (#12906)
* Add plan-mode command blocklist to run_commands

Plan mode kept run_commands available (needed for read-only
investigation) but relied on prompting alone to prevent file edits,
and weaker models routinely ignore that. Add a hard guard in
@cline/core's createShellTool that inspects each command before
execution and rejects file-editing constructs with a plan-mode tool
error instead of running them.

The guard is a quote/heredoc-aware scan that blocks file-manipulation
commands (rm/mv/cp/tee/touch/...), in-place editors (sed -i, perl -i,
gawk -i inplace, sort -o), output redirection to files (allowing /dev
sinks and /tmp for the documented output-capture pattern), mutating
git subcommands, package-manager installs, find -delete/-exec, and
nested command strings (sh -c, eval, sudo, xargs, ...). Windows and
PowerShell equivalents are covered too.

Enabled via a new blockFileEditingCommands flag on DefaultToolsConfig,
set by the plan tool preset (CLI and core runtime) and plumbed through
the VS Code extension's custom run_commands tool from the session mode.
The tool description and PLAN_MODE_INSTRUCTIONS now state the hard
block so models are forewarned.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Simplify plan-mode command guard to a plain blacklist

Replace the char-by-char shell tokenizer (heredoc queues, process
substitution, recursion into sh -c/eval, find -exec analysis) with a
simple scan: mask quoted text/heredoc bodies/escapes/comments so they
cannot false-positive, split on shell separators, and compare the
leading command word of each part against flat blacklists (commands,
mutating subcommands, in-place edit flags), plus one redirect check.
Quoted nested commands (bash -c 'rm x') are a documented false
negative. Also drop the guard from the package's public exports; it
is internal to createShellTool.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Move plan-mode command guard into a built-in beforeTool hook

Review feedback (abeatrix): command blocking is session policy, not
shell-executor configuration. Replace the blockFileEditingCommands
flag threaded through preset -> tool config -> VS Code host with a
core extension registered by the runtime builder for plan-mode
sessions. The beforeTool hook intercepts every run_commands tool in
the runtime - the SDK builtin, host replacements like the VS Code
terminal tool, and delegated sub-agents - and rejects file-editing
calls with the plan-mode error before tool policy and user approval,
so users are no longer prompted to approve a command that would only
fail. All VS Code wiring for the guard is removed.

Also adds block telemetry (sdk.plan_mode_command_blocked with the
blocked construct, never raw command content), per review.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Address review feedback on the plan-mode command blacklist

False positives (mkondratek):
- perl -Ilib / uppercase value-taking flags no longer match the
  in-place check; the flag cluster must end at a lowercase i
  (sed -Ei still blocked)
- awk inplace detection is tied to the -i/--include flag instead of
  matching the substring anywhere (filenames like inplace-notes.txt
  no longer trip it)
- read-only git forms allowed: stash list/show, worktree list,
  submodule status/summary, and any git subcommand with --help/-h
- arithmetic expansion (1) is masked before the redirect scan

Hardening and coverage (mkondratek, dominiccooney):
- temp-path redirect allowance rejects .. traversal (/tmp/../...)
- Windows gets a temp escape hatch: %TEMP%/%TMP%/$env:TEMP redirect
  targets are allowed and the block error mentions it
- curl -o/-O/--output/--remote-name and wget downloads blocked
  (--spider and -qO- stdout forms stay allowed)
- python -m pip resolves to the pip subcommand check
- unambiguous PowerShell aliases (mi, ri, cpi, rni, ac, clc) plus a
  case-variant test
- more package managers: winget, nuget, gem, composer, dotnet add,
  go install/get; bare classic yarn blocked again
- find -exec/-execdir/-ok chains and xargs -I {} placeholders are
  checked for mutating commands

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 20:35:54 -07:00
Saoud Rizwan f77d0930ba fix(llms): preserve models.dev reasoning options in generated catalog so adaptive-era Claude models never get manual thinking (#12908)
* chore(llms): regenerate model catalog from models.dev to pick up reasoning options

The baked fallback catalog was last regenerated before toModelInfo started
mapping models.dev reasoning_options into ModelInfo.reasoningOptions, so it
carried no reasoning metadata. Whenever the live models.dev fetch fails or a
model resolves from the baked catalog, adaptive-era Claude models (4.6+/5.x)
fell through the missing-reasoningOptions path to Anthropic manual thinking
and the API rejected the request with 'thinking.type.enabled is not
supported'.

This regen also picks up upstream models.dev drift; test expectations that
hardcoded stale catalog values (GLM 5.2 context window, OpenRouter GLM 4.7
reasoning controls, Vercel AI Gateway Qwen 3.6 Plus budget controls) are
updated to the current published values.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): infer adaptive thinking for adaptive-era Claude ids when catalog reasoning options are missing

Claude 4.6+ and 5.x models reject the manual thinking wire shape
(thinking.type 'enabled') on the Anthropic API. When a model resolves
without reasoningOptions metadata (offline baked catalog before the regen,
or user-typed unlisted ids such as claude-opus-4-6:1m), the reasoning
policy previously fell through to anthropic-manual and every
reasoning-enabled request failed with a hard API error.

Add isClaudeAdaptiveEraModelId as a narrowly scoped id fallback (name-first
Claude ids with version 4.6+ or 5.x, plus the Fable line) and use it in the
missing-reasoningOptions branch of resolveAnthropicReasoningRequestPolicy.
Genuinely old or unknown Claude-compatible ids keep the manual default,
which remains the safe shape for third-party Claude-compatible endpoints.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): prefer adaptive thinking over manual when a model advertises an effort control

A numeric reasoning.budgetTokens (e.g. a thinkingBudgetTokens setting
migrated from the legacy extension) used to force the anthropic-manual
policy whenever the model advertised a budget_tokens control. Claude 4.6+
models advertise both effort and budget_tokens on models.dev but reject
thinking.type 'enabled' on the Anthropic API, so those requests failed.
Effort now wins: adaptive is selected and the numeric budget is ignored.
Budget-only models (Sonnet 4.5 and older) keep honoring explicit budgets
via the manual shape.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(llms): guard baked catalog reasoning options for adaptive-era Claude models

Resolve adaptive-era Claude models through the generated (offline fallback)
catalog and assert their entries carry effort reasoning options that the
Anthropic reasoning policy resolves to adaptive thinking.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(core): update GLM 5.2 context window to current models.dev value

The catalog regen picked up upstream drift: models.dev now publishes a
1,000,000-token context window for zai/glm-5.2 (was 1,040,000).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert(llms): drop the Claude id-based adaptive-thinking fallback

Keep the fix surface minimal: the catalog regen covers every model
models.dev lists (the overwhelming share of the production failures), and
the effort-over-budget policy covers listed models that advertise both
controls. Unlisted id variants (e.g. claude-opus-4-6:1m) keep the
pre-existing manual fallback rather than introducing id-version parsing in
model-facts.ts; if they appear in models.dev the catalog picks them up
automatically.

This reverts commit 6d725b1ecd7c896a08c3c58dbb37afeaf34bb31e, keeping the
regenerated catalog and the effort-precedence change.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): default unknown Claude ids to adaptive thinking when catalog options are missing

Reintroduce the id-based fallback with the forward-compatible policy the
ecosystem converged on (vercel/ai#17804 for @ai-sdk/anthropic's capability
lookup; opencode's transform.ts after repeated allowlist misses for
opus-4.7, sonnet-5, and opus-5): when catalog reasoningOptions metadata is
unavailable, treat unrecognized Claude ids as newer than the known model
list and use adaptive thinking, since new Claude releases reject the manual
wire shape. Known legacy families (Instant, 2.x, 3.x, and name-first 4.0-4.5)
keep manual, as do non-Claude Anthropic-compatible ids and unknown Claude
ids carrying an explicit numeric budget (a custom-endpoint signal).

Unlike the earlier reverted allowlist (which defaulted unknown ids to
manual), this fails open for future models: claude-opus-4-6:1m-style
variants and next year's Claude work without a code change.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 20:34:13 -07:00
Saoud Rizwan d1462cf919 fix(llms): retry empty model turns on all providers, not just Ollama (#12927)
* fix(llms): retry empty model turns on all providers, not just Ollama

Production telemetry shows 'Model returned empty response' hard failures
on hosted backends (openrouter, cline, openai-compatible endpoints), not
just local Ollama — 46 tasks / 120 events in 24h on the SDK extension vs
~0 on legacy, which has its own empty-response fallback.

The retry-empty-response middleware already existed but was wired only
into the Ollama vendor. Move the wrap to the central AI SDK composition
point (createAiSdkProvider in ai-sdk.ts), where every vendor's model is
constructed, so all providers get it: retry only when a turn produced
genuinely nothing (no text, no reasoning, no tool call), tool-call-only
turns are never retried, non-empty turns stream through live, and error/
token-limit finishes pass through unchanged. Vendors can opt out or tune
attempts via ProviderFactoryResult.retryEmptyResponses. The agent
runtime's loud failure after persistently empty turns is unchanged.

* docs(sdk): drop changelog edit — release commits own the changelog

v0.0.69 is already published; its section must not be edited
retroactively. The next release commit will describe this change.

* ci: re-trigger checks (flaky Windows runner test timeouts)

* ci: re-trigger checks (flaky Windows runner test timeouts)

* fix(llms): classify stream parts exhaustively, buffer retry attempts, aggregate usage

Review follow-up (dominiccooney): the retry predicate and the response
parser were two independent, incomplete interpretations of the
LanguageModelV4StreamPart union, and rejected attempts leaked structural
parts and dropped billable usage.

- stream-part-classification.ts is now the single exhaustive boundary:
  every part is converted content, explicitly unsupported output,
  structural metadata, stream-start, finish, or error, with a never
  check so new AI SDK part types fail compilation. Retry eligibility
  derives from it: only turns with no output at all are retried;
  unsupported-but-real output (custom, reasoning-file, source,
  provider-executed tool-result) is never retried.
- Generated file parts are converted end to end: emitAiSdkEvents emits
  a new file AgentModelEvent and the agent runtime assembles it onto
  the assistant message (image part for image/*, file part otherwise),
  so a file-only turn is no longer an empty message. The legacy
  ApiStream bridge explicitly skips file events (no chunk type).
- Each retry attempt is buffered until it proves non-empty (first
  output or error part), so discarded attempts leak nothing — one
  retried request produces one clean stream with exactly one
  stream-start.
- finish.usage from discarded attempts is aggregated field-by-field
  (cache and reasoning detail included) into the emitted finish, so a
  three-request turn reports three requests' worth of tokens.
2026-08-04 20:33:06 -07:00
Saoud Rizwan 49d33aa793 fix(vscode): show cwd-relative tool paths in the chat view (#12900)
* fix(vscode): show cwd-relative tool paths in the chat view

The SDK message translator copied the model's absolute file paths straight
into the ClineSayTool messages, so chat cards like "Cline wants to read this
file" showed full absolute paths. Relativize them against the task's cwd for
display (classic getReadablePath behavior: relative inside the cwd, basename
for the cwd itself, absolute when outside), including apply_patch's
"*** Update File:" markers which DiffEditRow parses for its headers.

Also restores the readFile card's click-to-open target by setting content to
the absolute path, matching the classic extension.

* refactor: apply display-path relativization as a single ClineSayTool transform

Instead of threading cwd through every case of sdkToolToClineSayTool, leave
the tool mapping untouched and apply one toDisplaySayTool transform (with a
filesystem-path tool whitelist) at the points where tool cards are emitted.
Same behavior, much smaller footprint; MCP/unknown tools keep their exact
prior behavior.

* fix: keep absolute readFile open-target untouched on Windows; match '..' as whole segment

path.resolve(cwd, absPath) rewrites a drive-less absolute path onto the
current drive on Windows, breaking the readFile card's click-to-open target
(and the tests asserting it). Guard with path.isAbsolute instead.

Also match '..' only as a whole path segment in toDisplayPath so an in-cwd
entry literally named '..config' is not misclassified as outside the cwd
(greptile P1).

* fix: keep Desktop-fallback paths absolute; relativize '*** Move to:' destinations

When VS Code has no workspace open, getWorkspaceRoot() falls back to the
Desktop; classic getReadablePath deliberately keeps full absolute paths in
that case so the user can see where operations occur. Restore that guard in
toDisplayPath.

Also enroll PATCH_MARKERS.MOVE in relativizePatchPaths so a rename renders
both source and destination relative (covers the split-patch path too).
2026-08-04 20:03:07 -07:00
Saoud Rizwan 9b5dcc6405 Fix Bedrock prompt caching: emit Converse cachePoint markers instead of anthropic cache_control (#12928)
* Fix Bedrock prompt caching: emit Converse cachePoint markers instead of anthropic cache_control

The Bedrock provider manifest routed prompt caching through the
anthropic-cache-control format, so requests carried cache_control
provider options that @ai-sdk/amazon-bedrock silently drops - its
Converse message converter only reads providerOptions.bedrock.cachePoint.
Bedrock never received a cache checkpoint, cacheRead/cacheWrite were
always 0, and a stray top-level cache_control field leaked into the
Converse request body.

Adds a bedrock-cache-point prompt-cache format that attaches a
message-level cachePoint marker to the last user message, which the
converter appends as a cachePoint content block, caching the whole
prefix up to it.

Fixes #12913

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Format gateway.test.ts assertion

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 20:00:16 -07:00
Bee f2ac1ef10a fix(desktop): show skills in slash command menu (#12894)
* fix(desktop): show skills in slash command menu

* fix(core): normalize runtime slash command names

* fix(core): disambiguate colliding slash commands

* fix(core): preserve same-kind slash commands

* fix(core): stabilize colliding slash command aliases

* fix(core): avoid slow runtime command regex

* fix(core): remove quadratic hyphen trim

* fix(core): prefer skills over workflows on slash command collisions

Workflows are effectively deprecated in favor of skills, so when a
workflow's normalized name collides with a skill the skill now owns the
token and the workflow is dropped. This removes the collision
qualification machinery (-skill/-workflow/-hash aliases), which silently
renamed established CLI and VS Code command tokens, and removes the
duplicate-token throw that sat in the CLI send path, the hub snapshot
capability, and the desktop list_user_instruction_configs command.
Same-kind collisions resolve to the first entry of the deterministic
(name, id) sort, stable across discovery order.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): resolve typed workflow filenames through record ids

Name normalization broke the legacy /my-workflow.md fallback for
workflows renamed via frontmatter: the configured record name (e.g.
"Ship It") no longer compares equal to the normalized command token
("ship-it"), so a typed filename stopped expanding. Match the discovered
record to its runtime command by the stable record id instead, keeping
the canonical-name comparison as a fallback for callers that pass
records without ids.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): normalize snapshot names in hub slash command proxy

The hub-side proxy normalizes the typed token but compared it against
snapshot command names verbatim. Snapshots served by older clients carry
raw configured names (e.g. "Ship It"), which could previously exact-match
typed input and would now never match. Normalize both sides of the
comparison so mixed-version hub setups keep resolving.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): expand slash commands in the sidecar send path

Selecting a skill or workflow from the desktop slash menu inserted the
token but the sidecar dispatched it verbatim, so the model received
literal text like '/publish-ui write docs' instead of the configured
instructions. handleSend now expands a leading runtime slash command via
the core user-instruction service before dispatch (mirroring the CLI's
buildUserInputMessage), keeping the raw token as the session's display
prompt. Built-in webview commands (/fork, /team) and unknown tokens pass
through unchanged, and discovery failures fall back to the raw prompt.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): expand slash commands when editing queued prompts

Editing a pending prompt stored the raw slash token, which the runtime
later delivered to the model unexpanded — only the initial send path
went through expandRuntimeSlashCommand. handleUpdatePendingPrompt now
expands a leading skill/workflow token before persisting the update,
matching the enqueue behavior in handleSend.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): preserve Unicode letters in slash command tokens

Normalization stripped all non-ASCII characters, so a skill named 发布
got an unrelated generated token while typing /发布 could never resolve —
a regression from pre-normalization behavior where the exact name
matched. Keep Unicode letters and numbers in normalized tokens and only
collapse whitespace and symbol runs into hyphens.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-04 19:59:05 -07:00
Saoud Rizwan 10da3bf5d6 fix: emit AI SDK 7 image shapes and correct the claude-code peer range (#12901)
* fix(shared): emit AI SDK 7 file parts for images

formatMessagesForAiSdk still built the retired shapes: user images as
{type:'image'} message parts and tool-result images as {type:'image-data'}
content parts. AI SDK 7 auto-migrates both at runtime, but logs a
DeprecationWarning through process.emitWarning on every image-bearing
request, and the shims are slated for removal in the next major.

Emit the canonical shapes instead: {type:'file', data, mediaType} for
user images and {type:'file', data:{type:'data', data}, mediaType} for
tool-result media. mediaType is required on file parts, so URL-backed
images without a known type use the bare 'image' top-level segment,
which AI SDK 7 resolves per provider.

* fix(llms): allow the AI SDK 7 major of ai-sdk-provider-claude-code peer

The AI SDK 7 upgrade moved the ai-sdk-provider-claude-code
devDependency to ^4 but left the peer range at ^3.4.3, so consumers
resolving the peer would install the AI SDK 6 (Provider V3) major.
Align the peer range with the version the package is built against.
2026-08-04 19:53:51 -07:00
Saoud Rizwan 0034efe48c fix(llms): send max_completion_tokens for reasoning models on OpenAI-compatible endpoints (#12902)
* fix(llms): send max_completion_tokens for reasoning models on OpenAI-compatible endpoints

* fix(llms): require leading boundary in gpt-5 model-id pattern

* docs(llms): add maintenance notes to reasoning-era model-id patterns
2026-08-04 19:42:44 -07:00
Saoud Rizwan 64993e78d5 fix(llms): substitute image content for models without image support (#12903)
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 17:34:44 -07:00
Saoud Rizwan 06f31f2821 fix(llms): route Bedrock foundation models through geo inference profiles (#12926)
* fix(llms): route Bedrock foundation models through geo inference profiles

AWS Bedrock offers no on-demand throughput for newer foundation models;
they must be invoked through an inference profile. The SDK Bedrock vendor
passed model ids through unmodified, so every request with a bare modern
model id (e.g. anthropic.claude-sonnet-4-6) failed with "Invocation of
model ID ... with on-demand throughput isn't supported".

Resolve the wire-level model id in the Bedrock vendor: honor the existing
useCrossRegionInference / useGlobalInference settings (already plumbed
through provider config but previously ignored), and auto-prefix bare ids
of models known to have no on-demand throughput so they work without the
toggle. Ids that are already profile-prefixed, ARNs, and custom-model
configurations are never rewritten; unknown regions fall back to the raw
id. Country profiles (jp./au.) are preferred over apac. where the model
catalog shows AWS ships them.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): future-proof Bedrock profile-required model patterns

Match Anthropic tier-first naming generically (excluding the frozen
legacy claude-3-*/claude-v2/claude-instant naming schemes) instead of
enumerating tier names, so future profile-only Claude tiers work without
pattern-list updates. Also cover the profile-only Amazon Nova 2 series.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): gate Bedrock geo profiles on catalog availability

Address review feedback on inference-profile resolution:

- Never manufacture apac. (or other unconfirmed) profile ids: prefer a
  catalog-confirmed variant among the region's candidates (jp./au./apac.),
  and otherwise keep the raw id so AWS returns the actionable on-demand
  error instead of "provided model identifier is invalid". Profile-only
  models still fall back to us./us-gov./eu. prefixes, where AWS reliably
  ships geo profiles for such models.

- Drop the customModelBaseId short-circuit: legacy migration copies the
  base id without the custom-selected flag, so its presence must not
  disable profile routing for a normal catalog model. Custom/provisioned
  ids stay raw on the cross-region path because no catalog variant can be
  confirmed for them, and ARN-based custom models were already passed
  through.

Adds a per-region wire-id table test, an injected-catalog apac test, and
a stale-customModelBaseId regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): require catalog confirmation for every Bedrock geo profile

Remove the us./us-gov./eu. pattern fallback: AWS documents
inference-profile availability per model and geography, so no geographic
prefix is assumed valid without a catalog-confirmed variant (the catalog
had bare amazon.nova-lite/micro/pro ids with no geo variants, which the
fallback would have rewritten to unconfirmed eu./us. ids). The pattern
list now only gates eligibility for automatic routing; the catalog
always picks the actual prefix, and the raw id is preserved when no
variant is confirmed.

Adds boundary tests asserting pattern-matched models without confirmed
variants stay raw (with and without cross-region inference), plus
injected-catalog positive tests for us-gov. and future tier-first ids.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 17:34:13 -07:00
Saoud Rizwan 71e6b44ef7 Consolidate plugin display-name resolution into @cline/shared (#12905)
* Fix installed plugins all displaying as "index" in the desktop app

Hoist getPluginDisplayName (nearest-ancestor package.json name with
basename fallback) into @cline/shared storage paths, re-export it via
@cline/core, and replace the duplicated copies in cline-hub, the CLI
TUI, and VS Code marketplace helpers. Fix the desktop sidecar and
'cline config plugins', which still named plugins by entry-file
basename, so package-backed installs showed up as "index".

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Use shared getPluginDisplayName in desktop sidecar after #12933

Merging main brought in PR #12933, which fixed the desktop plugin
naming with another local copy of the helper. Drop that copy in favor
of the shared @cline/shared implementation this branch introduces, and
remove the node:path imports it needed.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 17:25:36 -07:00
Saoud Rizwan 0bcd602150 fix: task with attachments not resetting on New Task click (#12924) (#12937)
The webview posts an optimistic say:'task' message carrying the user's
images/files, and only clears it once an identical authoritative message
arrives from the extension. emitInitialTaskMessage omitted attachments,
so the optimistic copy was never confirmed and withPendingUserMessage
kept re-injecting the old task into the transcript even after New Task
cleared it - leaving the chat permanently stuck on the previous task.

- Include images/files on the authoritative initial task message so the
  optimistic pending copy is confirmed and cleared as designed.
- Defensively drop any unconfirmed optimistic message in startNewTask so
  an explicit New Task click always yields a clean slate.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 17:02:27 -07:00
Saoud Rizwan d759f4c646 fix(cli): track external git branch changes in the TUI status bar (#12930)
* fix(cli): track external git branch changes in the TUI status bar

The branch shown below the prompt was read once at startup and only
refreshed after an agent turn, so checkouts made from another terminal
or an editor left the TUI showing a stale branch (#12911).

Watch the repo's git dir for HEAD changes (git replaces HEAD via
rename, so a directory watch is used) and refresh the status bar
immediately, with a slow 5s poll as a fallback for filesystems where
fs.watch is unreliable. State updates are skipped when nothing changed
to avoid needless re-renders.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(cli): replace subprocess polling with stat-based HEAD backstop

Drop the unconditional 5s git-subprocess poll from useRepoStatus. The
fs.watch directory watcher stays for instant updates where the runtime
delivers HEAD events, but Bun on Linux drops them, so add fs.watchFile
on the HEAD file as the backstop: one in-process stat() every 2s that
only triggers git subprocesses when HEAD actually changed. Verified in
the Bun-run TUI that external checkouts show up within ~2s.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(cli): simplify HEAD watching to a single fs.watchFile

Drop the fs.watch directory watcher (Bun on Linux never delivers its
HEAD events, making it dead weight on the runtime the CLI ships on) and
the debounce it required. watchGitHead now just stat-watches the single
.git/HEAD file via fs.watchFile, which survives git's rename-based HEAD
updates and works on network mounts.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(cli): use a plain 5s poll for repo status

Remove the HEAD watcher entirely per review preference for minimal
code: root.tsx now just polls readRepoStatus every 5 seconds, skipping
state updates (via isSameRepoStatus) when nothing changed so idle ticks
don't re-render the app.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): skip repo status poll ticks while a read is in flight

Bounds concurrent git subprocesses when a read exceeds the 5s interval
(slow git on huge repos) and prevents an older completion from
overwriting newer status. Addresses Greptile review feedback.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 16:50:30 -07:00
Saoud Rizwan b035bf9255 fix(desktop): give chat message actions breathing room under the last line (#12921)
The copy/fork (and user copy/edit/restore) action row was pulled up 8px
(-translate-y-2), which made the icons collide with the descenders of the
message's last line of text. Reduce the raise to 4px (-translate-y-1) so the
actions sit with a small, deliberate gap under the message content while
still hugging the message closely enough to read as attached to it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 16:49:09 -07:00
Saoud Rizwan ca9cb7f554 Slow hero heading verb rotation from 2.6s to 5s (#12940)
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 16:27:12 -07:00
Saoud Rizwan 1ae2f71a0f fix(telemetry): dedupe sdk.error across layers and rate-limit repeated failures (#12931)
* fix(telemetry): dedupe sdk.error across layers and rate-limit repeated failures

Every provider failure was emitted twice — once by the model layer
(provider.stream, handled: true) and again verbatim by the agent loop
(agent.run, handled: false) — and unattended retry loops emitted the
same failure every iteration, unbounded. 24h of CLI data: 300K events
from 1.7K users, top 10 machines at 70% of volume.

Two changes:

- The agent loop no longer re-reports model stream failures. run-failed
  events carry errorClass exactly when the run failed on a model stream
  error, and the model layer already reports those at its own error
  boundary — so the run loop reports only failures that originate in
  the loop itself (empty response, max iterations, ...).

- captureSdkError caps identical failures per process: 5 per hour per
  (event, component, operation, error_type, normalized message), with
  digit runs collapsed so retry counters coalesce. Suppressed emissions
  surface as suppressed_count on the next emission after the window
  rolls over. In-memory only; the cap never blocks reporting.

Event name, attributes, and all call sites are unchanged;
suppressed_count is the only additive field.

* fix(telemetry): make sdk.error dedup ownership explicit and key limiter on status/code

Review follow-ups (#12931):

- Reporting ownership is now an explicit signal instead of being inferred
  from errorClass. captureSdkError returns whether the failure was
  recorded, the model layer forwards that as errorReported on the finish
  event, and the run loop skips only failures marked reported. Custom
  AgentModel implementations that never call captureSdkError leave the
  bit unset, so their failures still produce exactly one sdk.error from
  the run loop (regression test added).

- The rate-limit key now includes the structured error_status and
  error_code that normalizeSdkError already extracts, so an HTTP 429
  hot loop cannot consume an HTTP 401's budget even though their
  messages differ only by digits (tests added for both fields).

- resetSdkErrorRateLimiterForTests is tagged @internal; it stays
  re-exported because package test suites can only reach it through the
  package entry point.
2026-08-04 16:25:44 -07:00
Saoud Rizwan 62c57a0ccd ci(ui): publish @cline/ui without a manual approval gate (#12938)
The publish job ran under the shared `Publish` GitHub environment, whose
required reviewers turned every @cline/ui release into a two-person
ceremony. Nothing in the job reads secrets from that environment — it
authenticates to npm purely over OIDC trusted publishing — so the
environment bought us an approval prompt and nothing else. sdk-publish
and cli-publish already publish unattended the same way.

The npm trusted publisher for @cline/ui was registered with
`environment: Publish`, which pins the OIDC token's environment claim, so
it has been re-registered without it (same repo, workflow file, and
permissions). That change is already live; landing this without it would
have broken publishing.

Access is still gated by workflow_dispatch (write access required), the
`refs/heads/main` ref check, and the typed `publish` confirmation.
2026-08-04 16:23:46 -07:00
Saoud Rizwan f3c8b6748b Remove model-initiated plan-to-act switching from the VS Code extension (#12929)
* Remove model-initiated plan-to-act switching from the VS Code extension

Match the legacy extension: the model can no longer call switch_to_act_mode
to move itself from plan mode to act mode. The user must flip the Plan/Act
toggle manually. The CLI keeps the tool and its prompt unchanged.

- Stop registering the switch_to_act_mode extra tool in plan-mode sessions
  and drop the pending-mode-change queue, beforeModel stop hook, and idle
  apply path that existed only for the tool-initiated switch.
- Add a planModeSwitchTool option to buildClineSystemPrompt (default true,
  CLI output unchanged) and a PLAN_MODE_INSTRUCTIONS_MANUAL_SWITCH variant
  that directs the model to ask the user to toggle to Act mode instead of
  calling a tool it does not have; the extension passes false.
- The user-driven toggle path (togglePlanActModeProto), including
  auto-continue when a completed plan is presented, is unchanged.

* Use a generic completing-tool name in translator retag test

Review feedback: submit_and_exit is a yolo-mode tool and does not exist
in plan/act sessions. The test exercises tool-agnostic translator
behavior, so use a neutral example name and clarify the comment.
2026-08-04 16:09:05 -07:00
Bee e6104cfd2c feat(desktop): capture application errors in telemetry (#12893)
* feat(desktop): capture application errors in telemetry

* fix(desktop): deduplicate errors across reporting layers

* fix desktop error telemetry fallback

* fix(desktop): skip idle transport-close reports and reuse http endpoint helper

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 15:42:28 -07:00
Bee 12431cd97b fix(cli): claim connector instance before socket connect (#12765)
* fix(cli): claim connector instance before socket connect

Prevent racing foreground or detached connector launches from
both opening socket-mode with the same bot token by exclusively
claiming the state file via tryClaimConnectorStateFile before
connecting, and exit with CONNECT_ALREADY_RUNNING_EXIT_CODE when
another live instance already holds the claim.

* serializes stale-generation replacement without a removable mutex

* lint

* fix

* base

* fix(cli): keep pre-claim Slack state files manageable

State files written by CLI versions that predate connector claiming have
no claimId, so requiring it in readConnectorState made a live legacy
connector invisible: stop deleted its state without stopping it, which
let the next connect open a second socket-mode connection with the same
bot token. Treat claimId as optional metadata; claiming itself never
relied on the validator.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 15:41:19 -07:00
Bee 2b34b48ec6 fix(desktop): plugin package names (#12933) 2026-08-04 15:38:28 -07:00
Mikołaj Kondratek dc7eb755cf chore(telemetry): drop capture defs @cline/core owns, keep agent identity on late tool events (#12914)
* chore(telemetry): remove duplicate capture defs owned by @cline/core

The cline-core bundle layer (apps/vscode, which also compiles into the
cline-core.js that JetBrains runs) still carries event constants and
public capture methods inherited from the legacy architecture. Where
@cline/core now emits the same event, the bundle-layer twin is a second
capture path on the same telemetry service — which is how the
task.provider_api_error double-emission happened (cline/cline#12820,
follow-up to the removals in cline/cline#12818).

Removes 20 such methods and the EVENTS constants only they referenced:

- 15 whose signal @cline/core emits today (task lifecycle, tokens, tool
  and skill usage, auth start/success/failure, opt-out, workspace init,
  and summarize_task, which core replaced with task.compaction_*).
- 3 obsolete on both architecture lines: captureModelSelected (its
  model_selected signal survives as an action on
  captureOnboardingProgress), captureRulesMenuOpened, captureHostEvent.
- 2 whose trigger moved into core/sdk, so this layer can no longer
  observe them and re-wiring here would be wrong:
  captureWorkspacePathResolved (core already owns workspace.path_resolved)
  and captureGeminiApiPerformance (providers live in core; generic
  provider-timing events supersede it).

Deliberately NOT removed: capture methods with no caller here but a live
caller on legacy-extension. Those emit signals originating in this bundle
(webview UI, VS Code storage, host terminal, checkpoints, focus chain,
legacy-task migration), so core cannot emit them and the missing piece is
a call site on this line, not a redundant definition. They are the
SDK-parity backlog and are flagged as such in the file.

Verified against the JetBrains plugin repo: it references none of these
methods or event names, and no proto surface changes.

Also drops the unused TokenUsage interface, the taskTurnCounts and
taskToolCallCounts maps (only deleted methods wrote to them), and EVENTS
constants that were already orphaned before this change.

Tests that only exercised a removed method are gone; tests that used one
merely as a vehicle for provider/metadata assertions now use a surviving
method, so that coverage is preserved.

* fix(telemetry): keep agent identity on events dispatched after session teardown

A small share of task.tool_used events (~285 of 229k over 48h on
extension_variant=next) arrive without any agentId/agentKind/isSubagent
attributes. Root cause: AgentEventBridge.dispatchAgentEvent resolves
identity solely from the live-session map (AgentEvent metadata never
carries agentId in practice), and session teardown deletes the map entry
before the agent's run fully drains — dispose/stopSession paths can skip
or fail agent.shutdown() without aborting first. Late events from the
still-draining run then hit the session-map miss branch, which passed no
identity at all, so buildTelemetryAgentIdentity returned undefined and
the event was emitted bare.

Fix: snapshot the identity stamped on each session's events while the
session is registered (bounded FIFO map) and reuse it on a session-map
miss. Purely additive — no event is added, removed, or renamed; the
live-session and sub-agent paths emit byte-identical properties.
2026-08-04 22:44:19 +02:00
Bee accd7e5809 feat(sdk): add session initiation mode and lazy session persistence (#12807)
* feat(sdk): add session initiation mode and lazy session persistence

- Introduce top-level `StartSessionInput.mode` (`user`, `automation`, `subagent`, `team`) alongside `source`, so persisted history records both the client surface and how the session began; missing mode defaults to `user`.
- Make root-session persistence lazy: starting a runtime allocates the session ID in memory without creating a database row, manifest, or messages artifact. The first accepted user turn persists that same ID, so closing a runtime before any user turn leaves no empty history entry, and persistence never allocates a replacement ID for unknown sessions.
- Require automation runtime adapters to explicitly persist `mode: "automation"` for every run.
- Document the provenance model in `sdk/ARCHITECTURE.md`, update the VS Code session factory comment, and add tests for the automation runtime handlers.

* fix(sdk): persist automation trigger source as session provenance

The runtime adapters stopped writing the cron request source into the
session row when source became the client surface, which silently
dropped the spec-defined trigger label. Record it as
sessionHistoryOrigin.trigger instead, surface it in the messages-file
origin, and sort the new history-origin import in HubRuntimeHost.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-04 13:29:36 -07:00
Bee 400ba47387 fix(llms): switch ollama provider package (#12892)
* fix(ollama): use native AI SDK provider

* fix(ollama): patch ollama-ai-provider-v2 wire contracts and lock them with real-provider tests

The pinned ollama-ai-provider-v2@4.0.1 breaks four native Ollama wire
contracts (review findings on #12892). Patch the package via Bun
patchedDependencies:

- omit think from the request when no reasoning setting resolves,
  instead of forcing think: false (lets the server default apply)
- surface mid-stream {"error": ...} objects as error stream parts with
  an error finish reason, instead of dropping them before a clean finish
- serialize attachment-only user turns as string content (""), not []
- include the documented tool_name field on tool result messages

Add ollama.wire.test.ts exercising doStream through the vendor module
against the real (patched) package with a stubbed fetch, asserting on
the actual /api/chat request bodies and parsed stream so regressions in
the dependency's request converter or stream parser are caught.

* fix ollama model list refresh

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-04 13:28:53 -07:00
Bee 3af23c1c4c fix(cli,core): stop duplicate connector launches (#12770)
* fix: prevent duplicate connector launches during doctor/connect

Mark connectors as starting before the hub daemon spawns so autostart
skips in-flight instances, and improve doctor process filtering with
container-aware namespace/cgroup checks plus detached log rotation.

* Update apps/cli/src/connectors/common.ts

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

* feat(hub): supervise connector processes

* feat(connectors): enable tools by default, and stop replaying the Slack greeting

Tools were on by default only for Telegram (via --no-tools); Slack, Discord,
Linear, Google Chat and WhatsApp all required an explicit --enable-tools. All
six now default to tools on and opt out with --no-tools.

--enable-tools still parses everywhere, including Telegram which never accepted
it, so deployed scripts, systemd units and persisted autostart arguments keep
working. Passing both resolves to the safer answer: --no-tools wins. This also
affects hub/webview starts, which never emitted a tools flag and so ran those
five connectors with tools off.

Slack no longer posts the "Connected to Cline." first-contact message. It was
gated on per-thread welcomeSentAt, so a connector restart or a cleared history
made the next user message look like first contact and replayed the greeting.
The host mechanism is unchanged and the other adapters still greet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(connectors): recover a thread whose session is wedged mid-run

A connector thread keeps a long-lived mapping to a hub session. When that
session's runtime still had a run in flight and no abort had been requested,
every message in the thread came back as "SessionRuntime.shutdown called while a
run is in progress" instead of an answer, and stayed that way until someone
cleared the binding by hand. Observed on the Cline Mom Slack bot after a stack
restart.

The connector host already recovers from a session the hub no longer knows
about: it forgets the mapping and replays the turn once against a fresh session.
This widens the trigger from "session not found" to "session cannot serve
another turn" via isUnusableSessionError, so a wedged runtime takes the same
path.

The shutdown error now carries a stable code (SessionRunInProgressError,
session_run_in_progress) so callers can recognise it structurally. The predicate
also matches on message, because an error reaching a connector has crossed the
hub's JSON boundary and arrives as a bare message - and because a host commonly
runs a hub and CLI of different versions. Ordinary run failures still propagate
untouched: replacing the session on those would hide real errors and drop the
conversation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(connectors): serialise turns that share a session

Answering "what happens if I message the bot in another thread while it is still
replying": channel threads were already independent, but DMs were not.

findBindingForThread deliberately reuses one binding — and therefore one runtime
session — for every message in a DM channel, so a DM stays one continuous
conversation. The turn queue, though, was keyed by thread id, and a DM thread id
carries the message timestamp. Two messages in flight in the same DM therefore
got two independent queues and ran concurrently against a single session, which
fails with "shutdown called while a run is in progress" or interleaves two
conversations in one session history.

The queue key now follows the same identity rule as the binding lookup, via
resolveThreadTurnQueueKey next to findBindingForThread so the two cannot drift.
DM messages queue behind each other on the shared session; channel threads keep
their own key and still run in parallel. Applied to all six adapters, which all
had the same mismatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(core): abort an in-flight run before tearing its session down

Where the Slack bot's "plugin-sandbox process exited (code=null, signal=SIGTERM)"
came from, and its "shutdown called while a run is in progress" sibling: both are
one event, a session released while a run was still going.

stopSession aborts the agent first "so shutdown can proceed", but callers that
reach shutdownSession or releaseSessionRuntime another way did not - hub
dispose() on a restart being the one that hurt. Without an abort the runtime
refuses to shut down, that error is rethrown from the cleanup, and the plugin
sandbox is SIGTERMed while tool calls are still pending, so those calls reject
with "plugin-sandbox process exited". A connector turn awaiting the run reports
whichever surfaced first instead of answering.

Both paths now abort and let the run drain before shutting the agent, runtime and
sandbox down, guarded on session.aborting so callers that already aborted do not
abort twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(connectors): stop announcing "Steering current task."

Every follow-up sent while the bot was replying added an acknowledgement line to
the thread, and the wording overstated what happens: the host treats delivery
"steer" the same as "queue", enqueuing the prompt for the session rather than
injecting it into the loop already running. The follow-up is now handed over
silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(core): retire a dead supervised entry before replacing it

A start arriving while an instance sat in backoff left the old entry's
restart timer live. The timer closes over the old entry object, so when
it fired it spawned a second process for the same (channel, instanceId)
- untracked by the supervisor's map, so invisible to list() and
unreachable by stop() - two connectors holding one bot token, which is
the exact failure supervision exists to prevent. Its exit handler then
kept reaping the live instance's state and rescheduling restarts.

The same window exists before the timer is even scheduled: the
exit-cleanup chain runs first, and a replacement made mid-chain would be
followed by a restart scheduled for the retired entry.

start() now retires a dead existing entry explicitly - cancel its timer,
mark it stopped, drop its exit listener. Both the timer callback and the
cleanup chain already stand down on "stopped", so one mark covers both
phases.

* fix(core): serialise supervisor start/stop and wait for stopped processes to die

Found by exercising a hub restart against a live webhook connector: the
new hub's boot reconnect restarts the adopted survivor - which suspends
inside stop() on the CLI cleanup - while the user's `cline connect`
arrives as connector.start. With no per-instance serialisation the two
starts interleaved across that suspension and both spawned. The map
tracked one process while the other lived on untracked, holding the
connector's webhook port; the tracked chain crash-looped on EADDRINUSE
through all five attempts and ended state=failed, while the ghost kept
running with no way to reach it through list() or stop().

Two changes:

- start/stop (and the backoff-restart spawn) now run under a per-
  instance-key promise queue, so one instance has exactly one lifecycle
  operation in flight. The exit-cleanup chain also stands down when its
  entry is no longer the one in the map.
- stop() waits for the process to actually die after SIGTERM (bounded,
  then SIGKILL) instead of returning while it still holds its listen
  port - the race that turned the double-spawn into a crash loop, and
  that could burn a backoff cycle on any webhook connector restart.

process.kill is now injectable (killProcess), which also stops the test
suite from signalling arbitrary real pids like 600 on the host.

Verified live: the same kill-hub-then-reconnect sequence now converges
to one tracked running process, with the concurrent user start
correctly answered "already running under the hub".

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-04 13:28:18 -07:00
dependabot[bot] ce04e80909 chore(deps): bump rand (#11231)
Bumps the cargo group with 1 update in the /apps/examples/desktop-app/src-tauri directory: [rand](https://github.com/rust-random/rand).


Updates `rand` from 0.9.2 to 0.9.4
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/0.9.4/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/rand_core-0.9.2...0.9.4)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.9.4
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 12:53:15 -07:00
Saoud Rizwan 058c8c90a2 feat(desktop): ship a single universal macOS DMG instead of per-arch downloads (#12923)
Tauri's universal-apple-darwin target lipos the Rust binary but expects
sidecars to already be fat binaries, so build-sidecar-bin.ts now compiles
both Bun slices and merges them when the target triple is universal.
The publish workflow builds one universal bundle instead of a two-leg
matrix, verifies every Mach-O in the bundle carries both slices, and the
updater manifest points both darwin-aarch64 and darwin-x86_64 at the same
universal artifact so existing per-arch installs migrate automatically.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 12:42:55 -07:00
Saoud Rizwan 0f7e4b66ef Add themes to the CLI (#12899)
* Add user-selectable color themes to the CLI TUI

Adds a theme system to the interactive TUI (cline -i):
- New tuiTheme global setting persisted in global-settings.json
- Built-in themes: Auto (terminal-adaptive, default), Cline Dark,
  Cline Light, Tokyo Night, Gruvbox Dark, Nord, Dracula, Catppuccin
  Mocha, One Dark, Solarized Dark, Solarized Light
- /theme command, command palette entry, and a Theme row in
  /settings General tab, all opening a live-preview theme picker
- Named themes paint their background, default foreground, accents,
  syntax highlighting, and derived diff colors across the TUI
- CLINE_THEME env var overrides the persisted theme at startup

Closes #12872

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Widen theme picker dialog and label column

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Format theme picker

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Give each theme a descriptive picker blurb

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Theme all main-surface components instead of static palette colors

The ask-question / tool-approval element, toasts, queued prompts,
autocomplete dropdown, chat error cards, searchable lists, and the
onboarding screens hardcoded the brand palette (act blue, selection
highlight, black-on-selection text) and fixed dark grays, so they
ignored the active theme.

- ResolvedTheme gains selection/textOnSelection; the selected-row text
  flips between black and white by WCAG contrast against the accent
- Inline ask-question / tool-approval, Toast, QueuedPrompts,
  AutocompleteDropdown, SearchableList, and chat error cards now use
  theme accents and the themed selection pair
- Onboarding screens derive subtle borders/details from the theme
  background instead of #333333/#555555, and use themed accents
- Dialog surfaces (settings, pickers, history) intentionally keep their
  static dark surface styling

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-04 12:26:00 -07:00
Bee f1c45b63c8 feat(desktop): add token usage warning colors (#12919)
* feat(desktop): add token usage warning colors

* context info
2026-08-04 11:17:20 -07:00
Deach d6d1c789b3 fix: correct Linux keybinding label in Plan/Act mode tooltip (#11067)
* fix: correct Linux keybinding label in Plan/Act mode tooltip

On Linux, event.metaKey maps to the Super (Win) key, not Alt.
detectMetaKeyChar was returning "Alt" for Linux, causing the Plan/Act
mode toggle tooltip to display "Alt+Shift+A" instead of "Super+Shift+A".

Fixes #11026

* fix: update platformUtils.spec.ts Linux test expectation to Super

---------

Co-authored-by: Dominic Cooney <dominic.cooney@cline.bot>
2026-08-04 10:44:18 -07:00
oab24413gmai 3e81006863 docs: capitalize GitHub in security note (#11088)
Co-authored-by: Mira Sato <275437409+oab24413gmai@users.noreply.github.com>
2026-08-04 10:11:29 -07:00
John Choi 8ea52a2eea fix(desktop): make agent header draggable (#12910)
* fix(desktop): make agent header draggable

* fix(desktop): keep read-only title draggable
2026-08-04 09:21:22 -07:00
John Choi 5ec2d47b21 refactor(ui): extract agent prompt queue (#12791)
* feat(ui): extract agent prompt queue

* fix(ui): keep prompt queue usable on failures

* fix(ui): surface prompt queue action failures

- report failed edit, steer, and remove callbacks inline as a row alert
- disable all queue actions while any action is in flight
- mark the busy row aria-busy and accept readonly item arrays

* fix(ui): preserve prompt queue failures

* chore(ui): drop unrelated formatting changes

* refactor(ui): style prompt queue with Tailwind

* style(ui): remove redundant prompt queue reset
2026-08-03 20:01:37 -07:00
Saoud Rizwan 46fcde0a96 ci(desktop): drop the Rust build cache from the code-signing job (#12898)
* ci(desktop): drop the Rust build cache from the code-signing job

The `build` job is the only one that can read the Apple Developer ID
certificate and the Tauri updater signing key, and it restored a
swatinem/rust-cache archive before running them. A restored cache archive
is attacker-controlled the moment the Actions cache is poisoned, which is
the pivot used against this repo's nightly workflow in Feb 2026 and the
reason actions/cache was stripped from the credential-bearing publish
jobs at the time. This workflow was added months later and reintroduced
the pattern. The updater key is the worst thing here to leak: it signs
every auto-update the installed desktop app accepts.

The cache was also not buying anything. Across the eight runs of this
workflow, seven logged "No cache found" on both matrix legs; only the run
32 minutes after another one hit, saving 1-3 minutes. A release cadence
measured in days does not outlive the entry under the repo's 10 GB LRU
eviction, so the steady state was a cold build regardless. Cold builds
took 5-7 minutes against a 90-minute timeout.

No behaviour change otherwise: the step had no id and no outputs, so
nothing referenced it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* ci(desktop): trim the cache-removal comment to the constraint

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-03 19:16:23 -07:00
John Choi fbaa44be96 refactor(ui): extract agent ask question (#12790)
* feat(ui): extract agent ask question

* fix(ui): harden ask-question presentation

- dedupe repeated model-supplied options so keys stay stable
- mark pending items aria-busy to match the approval card
- expose the accent palette as overridable custom properties

* fix(ui): polish ask question feedback

* fix(ui): label follow-up question sections

* refactor(ui): style ask question with Tailwind
2026-08-03 18:55:12 -07:00
John Choi aec350bb9d refactor(ui): use Tailwind for shared components (#12719)
* refactor(ui): extract desktop approval card

* fix(ui): preserve approval card parity

* refactor(ui): keep approval labels fixed

* refactor(ui): use Tailwind for shared components

* test(ui): cover each Tailwind component source

* refactor(ui): migrate approval card styles

* fix(ui): preserve host and quick-action behavior

* fix(ui): preserve component hover behavior

* fix(ui): preserve selected option hover

* fix(ui): isolate embedded Tailwind contract

* fix(ui): reset approval button block padding

* chore(ui): bump preview package version
2026-08-03 18:28:57 -07:00
oab24413gmai 16129cf90a docs: capitalize GitHub in security note (#10723)
Co-authored-by: Mira Sato <275437409+oab24413gmai@users.noreply.github.com>
2026-08-03 17:34:27 -07:00
James Arlen 3520786f4a security: hygiene sweep — docs pin lifts, example next bump, workspace overrides (closes ~153 Vanta findings) (#12749)
* security: docs/examples/tooling hygiene sweep — lift fix-blocking pins, bump example next, workspace overrides

VMP 2026-07-30 quarterly run, PR 3 of the condensed worklist (closes ~153 Vanta findings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(deps): refresh lockfiles for security updates

* fix(deps): keep Discord on patched Undici 6

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-08-03 17:21:03 -07:00
Bee 8f0ff70215 chore(llms): upgrade to AI SDK 7 (#12891)
* chore(llms): upgrade to AI SDK 7

* fix(llms): include Codex provider during builds

* fix(llms): address AI SDK 7 runtime regressions
2026-08-03 17:10:41 -07:00
Saoud Rizwan 0619e5a016 fix(cli): deliver Telegram slash commands to the connector command host (#12888)
The @chat-adapter/telegram library intercepts any message whose leading
entity is a bot_command and routes it to slash-command handlers instead
of the mention/subscribed-message handlers. The Telegram connector
registered no onSlashCommand handler (unlike Discord and Slack), so
commands like /clear were consumed by the library and silently dropped.

Register a slash-command handler that rebuilds the originating chat
thread and forwards the original message text (preserving @bot
addressing for group chats) into the same turn pipeline as regular
messages, so connector commands reach the chat command host.

Fixes #12871

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-03 15:57:41 -07:00
Dominic Cooney 2a0dd197bf chore(vscode): remove dead next-gen model classifier (#12887)
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-08-04 07:10:04 +09:00
Tomás Barreiro 82e3596f96 Add a script to do dev work on ACP (#12886) 2026-08-03 22:21:16 +02:00
Octopus 895ab53b78 fix(llms): inherit MiniMax default from models.dev (#11218)
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-08-03 21:23:44 +02:00
Tran Binh Minh b8f51b9e55 fix(vscode): surface a clear error when a provider has no API key (#12859)
* fix(vscode): surface a clear error when a provider has no API key

A key-based provider with no API key sends the request without an Authorization header, and the provider's raw 401 reached the chat panel unclassified because reshapeErrorForWebview falls through to the raw message and ClineError's auth regexes do not match it. Rewrite the missing-Authorization-header case into actionable guidance naming the provider, alongside the existing model-not-found matcher. Matching is limited to the no-header signature so a present-but-wrong key is never relabelled as missing, and no preflight is added because authMethod misclassifies local providers and 175 of 179 builtins resolve keys from the environment.

* fix: don't name a fallback provider in the missing-key message

reshapeErrorForWebview defaults providerId to "cline" for its
ClineError-JSON branches, but state.activeProviderId() can be undefined —
the missing-credential message would then blame the cline provider for a
key it doesn't take. Keep the "cline" fallback for the JSON branches and
pass the raw id to the credential matcher, which now only names a provider
it was actually given.

---------

Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
2026-08-03 11:27:08 -07:00
Michael Gasperini 25dc89eab4 feat(vscode): recognize Chutes provider (#12068) 2026-08-03 11:16:09 -07:00
Mikołaj Kondratek 5fdd840d5f refactor(llms): classify typed AI SDK errors before the structural walk (#12814)
* refactor(llms): classify typed AI SDK errors before the structural walk

Add a typed pre-pass to classifyProviderError that recognizes real AI SDK
error instances via their symbol-based isInstance() guards: RetryError
unwraps to its last attempt, APICallError is judged on message/responseBody/
data with its typed statusCode as the sole authoritative status,
TypeValidationError on the payload in value, and any other AISDKError
recurses into its cause. The detection rules (overflow patterns, provider
codes, rate-limit vetoes, invalid-request status gate) are extracted into a
shared verdict function used unchanged by both the typed pass and the
existing structural walk, which remains the fallback for gateway-forwarded
plain-JSON payloads that only name an AI SDK error (ENG-2394).

* fix(llms): classify a RetryError by its final attempt even when untyped

When a RetryError's last attempt was not a typed AI SDK error, the typed
pre-pass fell back to structurally walking the whole wrapper, letting
signals from earlier (retried-away) attempts veto or fake the final
attempt's verdict — e.g. a retryable 429 on attempt one vetoing a plain
overflow rejection on the final attempt. Walk the final attempt alone
instead; a RetryError with no recorded attempts still falls back to the
plain structural walk.

* fix(llms): gate typed APICallError verdicts on the authoritative statusCode

verdictFromSignals checks the explicit context_length_exceeded code before
the rate-limit veto and the invalid-request status gate, so a typed
APICallError with statusCode 429 or 500 whose body echoed that code was
still classified as an overflow, contradicting the branch's contract that
the typed statusCode is the sole authoritative status. Gate the whole
payload verdict on the typed statusCode first (absent a statusCode the
payload still decides), and cover the explicit-code case at 429/500/400
with real instances.
2026-08-03 06:40:40 -07:00
Mikołaj Kondratek cdcaa74422 feat(sdk): detect and recover from context-window overflow errors (#12804)
* feat(sdk): detect and recover from context-window overflow errors

Port of the legacy arch's context-window-exceeded handling to the SDK
arch (the SDK arch previously surfaced these as raw unclassified stream
errors with no recovery; see ENG-2394 root-cause investigation).

- llms: new classifyProviderError() walks the raw provider error
  structure (AI SDK wrappers, gateway value.error_message, responseBody,
  cause chains) and classifies it before extractErrorMessage flattens
  it. Reuses the legacy detectors' message patterns with rate-limit
  vetoes and an invalid-request status gate.
- shared: ProviderErrorClass union; errorClass on the model finish
  event, run-failed event, runtime snapshot, and prepare-turn contexts.
- core: prepare-turn overflowRecovery flag forces a compaction that
  bypasses the token-estimate trigger (the estimate just proved wrong)
  and runs the deterministic basic strategy directly, so recovery never
  depends on another successful LLM request. New overflow_recovery
  compaction mode in status notices and compaction telemetry.
- agents: on a classified overflow the runtime force-compacts and
  retries once per run, emitting a status notice. Terminal states fail
  with actionable messages instead of raw provider dumps: nothing to
  compact (first-prompt overflow), no prepare-turn pipeline, or a retry
  that still overflows. The doomed request is not re-sent when forced
  compaction cannot shrink the transcript.
- telemetry: task.provider_api_error gains errorClass and
  task.provider_stream_failed gains error_class, populated from the
  same classification, so context-overflow failures become countable.

* fix(core): keep overflow-recovery compaction deterministic with custom compactors

A session-supplied compaction.compact previously took precedence over
the overflow_recovery basic-strategy branch, so an LLM-backed custom
compactor could hit the same context overflow mid-recovery. The custom
compactor still gets first shot (it sees mode overflow_recovery and
owns its transcript invariants), but if it throws or declines, basic
compaction now runs so recovery never depends on another successful
LLM request. Cancellation still propagates.

* fix(core): fall back to basic compaction when a custom compactor does not shrink during overflow recovery

A custom compactor that returns unchanged or larger messages would
previously satisfy the recovery branch, and the runtime would then
reject the retry as non-shrinking and fail terminally even though
basic compaction could still prune the transcript. Recovery now
treats a non-shrinking custom result like a decline and runs basic
compaction.

* fix(core): hold custom overflow-recovery compaction to the recovery token target

A custom compactor result that was only marginally smaller than the
input passed the shrink check, skipped the basic fallback, and spent
the run's single recovery retry on a request that still could not fit.
The custom result is now accepted only when it is strictly smaller AND
within the recovery token target basic compaction aims for; otherwise
basic compaction runs.

* fix(core): reject empty custom compaction results during overflow recovery

An empty transcript from a custom compactor passed both the shrink and
token-target checks (trivially smaller, zero tokens) and suppressed the
basic fallback, so the retry would have been sent without the request
it was supposed to re-send. The acceptance bar now covers the full
input space in one predicate: non-empty AND strictly smaller AND within
the recovery token target.

* feat(core): expose the turn abort signal to custom compactors

CoreCompactionContext now carries the prepare-turn abort signal, so a
custom compact implementation that calls a model or external service
can observe cancellation instead of blocking the turn (including the
overflow-recovery path) on a stalled request. Builtin strategies
already received the signal via providerConfig; this closes the gap
for custom compactors across auto, manual, and recovery modes.

* fix(sdk): classify provider errors from registered ApiHandler models

Registered handlers (VS Code LM and any other host-supplied provider)
reach the runtime through createAgentModelFromApiHandler, which flattens
failures to a message string — so context-window rejections on that path
were never classified and never entered overflow recovery.

- The adapter now classifies at its own error boundary, where the raw
  error is still structured (status codes, response bodies), for both
  thrown errors and failed done chunks. Aborts stay unclassified.
- The runtime falls back to classifying the finish message when a model
  supplies no class, so custom AgentModel implementations are covered
  too.
- Hold the custom-compactor acceptance check to token estimates on both
  sides instead of mixing serialized length with a token target, and
  document why the runtime's shrink backstop keeps a serialized-size
  proxy (the shared estimator is linear in characters, so the verdict is
  identical) with a TODO to surface real estimates from prepareTurn.
- Drop the now-unused errorClass parameter from captureProviderApiError:
  #12820 removed core's capture site, so host adapters own that event.

* test(core): reuse the handler harness for the overflow classification case

The hand-rolled throwing generator had no yield, which biome's
correctness/useYield rejects as an error (the repo's lint gate runs on
sdk/ and apps/, and biome does not honor the eslint require-yield
directive the existing harness carries). fakeHandler now accepts the
error to throw, so the new case reuses it instead.
2026-08-03 09:03:22 +02:00
Mikołaj Kondratek 5acc98474a fix(mcp): refresh lists on list_changed notifications instead of toasting (#12619)
* fix(mcp): refresh lists on list_changed notifications instead of toasting

Servers emit notifications/tools/list_changed in bursts (a toolset change
or shutdown can produce a dozen at once), and the fallback notification
handler surfaced every one of them as a host toast, flooding the user
with identical messages (ENG-2298, found testing the JetBrains IDE MCP
server integration).

Handle tools/resources/prompts list_changed notifications by refreshing
the corresponding cached lists, debounced 300ms per server and list
kind, then pushing the update through notifyWebviewOfServerChanges() so
the webview and the SDK session tool-list check pick it up. Downgrade
remaining unhandled notification types to logger output.

* fix(mcp): guard list_changed refreshes against races and failed fetches

Address review: serialize per-key refreshes by chaining onto any
in-flight one, so overlapping fetches can't complete out of order and
publish a stale list. Make the fetch helpers return undefined on
failure (instead of an empty list) so the refresh path can keep the
previous cached list and skip the webview notification, rather than
erasing valid entries on a transient error; connect-time call sites
keep their old empty-list fallback.

* fix(mcp): drop in-flight list refresh when the connection was replaced

Address review: refreshChangedList captured the connection object before
awaiting the list fetches, so a reconnect mid-fetch wrote the result to
the removed connection while the replacement kept its own state. Re-check
connection identity after the fetches and drop the result when it
changed — the replacement fetched fresh lists at connect time, after the
change that produced the notification, so the in-flight result is older.

* fix(mcp): retry failed list refreshes and publish state after reconnect

Address review. A list_changed notification consumes the server's change
signal, so a transiently failed refresh left the cached list stale until
the next notification; retry with exponential backoff (1s/2s/4s, max 3)
per server and list kind, with a fresh notification superseding any
pending retry. Also publish server state after a successful streamable
HTTP reconnect: connectToServer() loads fresh lists but never sent them,
leaving the webview on 'connecting' with pre-reconnect capabilities.

* fix(mcp): don't restart a live connection when post-reconnect publish fails

Address review: the post-reconnect notifyWebviewOfServerChanges() sat
inside the connect retry loop's try block, so a publication failure
(e.g. a settings file read error) was treated as a transport failure
and re-ran connectToServer() against the already-live connection,
leaking its client/transport. Publication now happens outside the
connect try/catch and only logs on failure.

* fix(mcp): drop superseded in-flight list refreshes instead of publishing

Address review: a newer list_changed notification queued its refresh
behind one already in flight without invalidating it, so the older run
could briefly publish an obsolete list (and churn the SDK session)
before the newer refresh corrected it. Each schedule now starts a new
generation per server+kind; a run whose generation is no longer current
skips fetching (when caught early), drops its result before publishing,
and doesn't schedule retries — the superseding refresh covers it.

* fix(mcp): harden list refresh and reconnect publication paths

Address review (post-reconnect publish failure leaving consumers stuck
on 'connecting' with stale lists) plus an adversarial pass over the
whole change to close the remaining gaps in one batch:

- Retry publications bounded (publishServerChanges) after a successful
  reconnect AND in both terminal disconnected paths, which are equally
  terminal; never throw from handleError, whose promise transport.onerror
  discards. Guard the stdio/SSE onerror publishes the same way.
- Treat an undeclared capability or a method-not-found answer as an
  authoritatively empty list instead of a retryable failure, so servers
  without e.g. resources/templates/list don't burn the full retry ladder
  on every list_changed notification.
- Retry when the fetch succeeded but the webview publish failed: the
  cache is updated but consumers haven't seen it.
- Cap debounce deferral at 2s so a sustained sub-300ms notification
  stream can't starve the refresh indefinitely.
- Cancel pending refresh timers in deleteConnection; return 'skipped'
  (not 'failed') when a fetch failure coincides with connection
  teardown or supersession, so no retry fires against a replacement
  connection that already fetched fresh lists.
- Clear the pre-existing toolListChangeDebounceTimer in dispose().

* fix(mcp): supersede in-flight refreshes during connection teardown

Address review: deleteConnection removes the connection from
this.connections only after awaiting transport/client close, so a list
refresh completing inside that window passed its identity check and
published state for a connection being torn down. Bump the per-key
generation at the start of deleteConnection so any in-flight refresh is
superseded and drops its result; bumping (never resetting) keeps
generations monotonic across reconnects.

* fix(mcp): close reconnect-retry and teardown-publication races

Address review (cline-cloud):

1. The streamable HTTP reconnect loop revalidated only after the first
   backoff. Later retries could resurrect a server removed or disabled
   from settings during a delay, or displace a replacement connection
   another path had installed — connectToServer() drops a same-name
   connection without closing it, leaking its transport. The loop now
   revalidates before every attempt: it aborts when a live replacement
   exists (our own original connection and the 'disconnected' husk left
   by our own failed attempt don't count) or when fresh-read settings no
   longer define the server as enabled (isStillWanted callback; a
   settings read failure keeps the chain alive).

2. deleteConnection removed the connection from this.connections only
   after awaiting transport/client close, so a publication passing its
   suspension points inside that window could still serialize and
   publish the dying connection's state. The connection is now removed
   from published state before the close handshake is awaited.

The exhausted-retries test's partial-connection mock now carries status
'disconnected', matching what connectToServer's error path actually
leaves behind — that status is what distinguishes our own husk from a
live replacement.

* fix(mcp): don't displace an OAuth-required replacement during reconnect retries

Address review: the retry loop's replacement guard treated every
'disconnected' connection as our own failed-connect husk. An
OAuth-required connection is also 'disconnected' but retains its
client, transport, and authProvider for authentication — a retry that
displaced it would orphan that session and clobber the pending auth
state. Distinguish by client presence: the husk's creation sites set
client: null, so a 'disconnected' connection holding a client is a
replacement and aborts the retry chain.

* fix(mcp): distinguish OAuth replacements by flag, not client presence

Address review: an ordinary post-registration connect failure leaves a
'disconnected' connection with its (already-closed) client still
attached, so the client-presence check classified it as an OAuth-style
replacement — aborting the reconnect chain after a single failure,
including our own retries. Discriminate on server.oauthRequired
instead: only the OAuth-required connection retains live
client/transport/authProvider state worth protecting; ordinary failed
connections closed their client before being marked disconnected, so
retrying past them displaces nothing live. The exhausted-retries test
mock now carries the real husk shape (closed client attached) to pin
this regression.

* test(mcp): cover retry succeeding after a failed attempt's registered connection

Requested in review: the guard must recognize the 'disconnected'
connection a failed non-OAuth connectToServer() leaves behind (closed
client still attached) as our own attempt, and the following retry must
proceed and succeed.

* fix(mcp): don't retry reconnects with a config settings no longer define

Address review: a retry reconnects with the config captured at
connection creation, so if the user changed the server's config during
a backoff delay (and the watcher's reconnect with the new config
failed, leaving a disconnected husk our guard rightly retries past),
the retry would resurrect the obsolete URL/headers/command — and a
successful stale connection would contradict settings until the next
file touch. isStillWanted now also compares the captured config against
current settings via configsRequireRestart (connection-relevant fields
only), aborting the chain when they differ: the settings watcher owns
reconnection after a config change.
2026-08-03 07:51:04 +02:00
Bee 53a5266239 feat(desktop): show token usage in input toolbar (#12803)
* feat(desktop): show token usage in input toolbar

Load per-model context window sizes from the provider catalog and
pass the active model's limit down to ChatInputBar. Render a token
ring that visualizes current token usage against the model's context
window, and hydrate token usage plus cumulative cost from messages
and chat_usage events so the indicator stays accurate across turns
and session reloads. Add tests covering the ring rendering and usage
hydration.

* bigger ring

* move submit button to input box

* fix

* add cost tracker

* fix

* fix queued turn cost tracking
2026-08-02 17:19:33 +02:00
Saoud Rizwan 1654517614 ci(desktop): gate desktop publish secrets behind PublishDesktop environment (#12854)
* ci(desktop): gate desktop publish secrets behind PublishDesktop environment

The Apple signing/notarization and Tauri updater secrets were repository
secrets, readable by any workflow in the repo and by anyone with push
access via a branch carrying a modified workflow. Move them behind the
PublishDesktop environment, which requires reviewer approval and
restricts deployments to main.

The build job now declares the environment, so those secrets are readable
only there and only after an approval. Add a preflight check because a
missing secret fails dangerously rather than loudly: Tauri silently skips
code signing when APPLE_CERTIFICATE is empty and skips notarization when
APPLE_API_KEY is empty, so a misconfigured environment would still
publish an unsigned, un-notarized bundle. Only a missing updater key was
already caught, by the .sig check in Collect artifacts.

validate stays ungated so a bad tag fails in seconds rather than after an
approval, matching the ungated-build/gated-publish split in
ext-vscode-ab-package. The shared Slack and telemetry secrets stay where
they are; scoping them to this environment would silently empty them in
the CLI, SDK, and extension publish workflows.

* ci(desktop): verify signing secrets are not repository-scoped

The preflight added in the previous commit checks that the signing
secrets are non-empty, which proves presence but not scope, and then
reported that they had resolved from PublishDesktop. An environment-gated
job resolves repository and organization secrets too — environment values
merely take precedence — so a credential left at repository level would
pass that check while the message claimed the migration had worked. This
workflow already demonstrates it: the gated build job reads the shared
Slack and telemetry secrets, none of which are on the environment.

Add the complementary check to validate, which declares no environment: a
signing secret that resolves there can only be repository- or
organization-scoped, so it fails the run and names the offenders. Neither
check establishes provenance alone; together they do. validate is
ungated, so a misplaced secret now fails before the approval rather than
after it.

Also drop the provenance claim from the build message and correct the
skill doc, which stated that a repository-level secret would be invisible
to the gated job.

Reported by greptile on #12854.
2026-08-02 01:55:24 -07:00
Saoud Rizwan bbfcbcd31d chore(vscode): bump to 4.1.3 for stable release 2026-08-01 22:11:03 -07:00
Saoud Rizwan 102ef1ab84 chore(desktop): release v0.0.8 2026-08-01 21:53:13 -07:00
Saoud Rizwan 55d1476169 chore(cli): release v3.0.49 2026-08-01 21:37:00 -07:00
Saoud Rizwan 6173bad65e chore(sdk): release v0.0.69 2026-08-01 21:11:32 -07:00
Saoud Rizwan cf710d76bf feat(llms): retry empty Ollama responses at the model boundary (#12845)
Local backends (Ollama especially) intermittently return a turn that
finishes normally but carries no text, reasoning, or tool call. In the
SDK runtime an empty assistant turn is a hard failure ("Model returned
empty response"), so one flaky generation kills the whole task.

Adds a LanguageModelV3 middleware that retries the stream only when a
turn produced genuinely nothing, wired as the outermost middleware on
the Ollama vendor. A tool-call-only turn counts as content and is never
retried; non-empty turns stream through live with no added latency; and
turns that error or hit the token limit are passed through unchanged.

This is the streaming-safe slice of ai-sdk-ollama's reliability story:
its own reliability layer lives in doGenerate and owns the tool loop
(executes tools and force-synthesizes text), which is incompatible with
Cline running its own loop over doStream.
2026-08-01 19:34:54 -07:00
Saoud Rizwan 9b31692fa0 fix(migration): fall back to the default Cline model for unknown legacy model ids (#12846)
* fix(migration): fall back to the default Cline model for unknown legacy model ids

Some migrated users ended up making Cline provider requests with a model
id the new extension doesn't have because the legacy migration carried
their stored model id over verbatim and never applied a default.

Two small fixes in the provider settings migration:

- Drop a legacy Cline model id the catalog doesn't know so the entry
  falls back to the default model instead of carrying the unknown id
  into inference requests.
- getDefaultModelForProvider only accepted defaults present in the
  generated model block; Cline's generated block holds a few free models
  while its declared default (anthropic/claude-sonnet-5) lives in the
  collection catalog, so the fallback previously landed on an arbitrary
  free model instead of the default.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(migration): validate legacy Cline models against the full runtime catalog

The known-model check used the curated Cline collection plus the tiny
generated cline block, but the runtime Cline catalog is OpenRouter-backed
and also resolves Vercel AI Gateway alias ids. Legacy users on
runtime-served ids outside the curated collection (e.g. the z-ai/glm-5
family) would have been wrongly defaulted. Suffixed variant ids like
...:1m still fall back to the default Cline model.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(migration): validate Cline models against the canonicalized runtime catalog

Greptile review: the raw generated-catalog checks accepted alias ids
(e.g. OpenRouter's z-ai/...) that buildClineModels canonicalizes away
(to zai/...), persisting models absent from the exposed runtime catalog.

Validate against the collection model list (which the runtime catalog
mirrors exactly) and fold alias spellings onto their canonical ids via
the shared VERCEL_OPENROUTER_MODEL_ID_ALIAS_RULES, so legacy z-ai users
keep their model under the canonical id instead of being defaulted.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-01 19:34:03 -07:00
Saoud Rizwan d3e32500ae fix(ollama): raise the response-start timeout default to 5 minutes so model cold loads don't error (#12839)
* fix(llms): retry Ollama response-start timeouts through the AI SDK retry loop

The pre-SDK handler wrapped Ollama chat calls in withRetry({ retryAllErrors:
true }), which silently rode out model cold loads: Ollama holds /api/chat open
while loading and only sends response headers once the model is ready, so the
first attempt of a large model routinely times out at 30s and a later retry
lands on the loaded model. The SDK path lost that behavior twice over: the
response-start timeout rejected with a plain Error (the AI SDK only retries
APICallError with isRetryable), and ai-sdk-ollama wraps every doStream failure
in its own OllamaError, hiding even a correctly-typed error from the retry
predicate. Net effect: one attempt, a surfaced timeout error, and no automatic
recovery - a regression vs the legacy extension for local models that load
slower than the timeout (cline/cline#12829).

Fix: withOllamaResponseTimeout now rejects with APICallError(isRetryable:
true) when its own timer fired (upstream aborts still propagate untouched),
and a restoreOllamaApiCallErrorMiddleware unwraps the buried APICallError from
OllamaError cause chains so streamText's built-in retry (2 retries with
backoff, ~96s of cold-load coverage) engages.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: biome format

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* rework: raise Ollama response-start default to 5 minutes instead of retrying

Replaces the APICallError/retry-middleware approach: the 30s guillotine was
the actual root problem (Ollama sends response headers only after the model
cold-loads; killing a healthy request forces error/retry churn), so give the
response-start budget the same order of generosity other AI SDK-based agents
use (opencode: no default header timeout for custom providers, 5 minutes for
its only default) and delete the retry machinery. Unreachable servers still
fail instantly at the connection level, users can still cancel from the UI,
and an explicit requestTimeoutMs is still honored.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert Ollama timeout description copy, keep the new default values

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: forward Ollama request timeout and context window to standalone handlers

Greptile review catch on #12839: buildSdkProviderConfig never carried
requestTimeoutMs, so handlers built via buildApiHandler (commit message
generation) ignored an explicit user timeout — pre-existing, but material now
that the fallback default is 5 minutes. Reuse the session factory's
resolveOllamaProviderConfig so the standalone path honors the configured
timeout and the user's context window (num_ctx) instead of Ollama's 4096
default, keeping the two paths on one source of truth.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-01 19:19:48 -07:00
Saoud Rizwan b0cb179a06 fix(settings): persist base URL and API key edits made before provider config loads (#12840)
OpenAI Compatible and LiteLLM gated their base URL onChange (and API key
writes via canWrite) on the async provider config having loaded. Text typed
in that window hit a no-op onChange after the debounce cleared the
pending-edit flag, so the late initialValue resync wiped it and nothing was
saved. write() never needed loaded config, and useProviderConfig's request
sequencing already drops the stale initial read, so the guards are removed.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-01 18:26:09 -07:00
Saoud Rizwan f57c7944ec fix(vscode): keep plan/act input border in sync with actual textarea focus (#12841)
Sending a message (Enter or send button) cleared the isTextAreaFocused
flag without blurring the textarea. Since the DOM element stayed focused,
onFocus never re-fired (programmatic .focus() on an already-focused
element is a no-op), so the mode-colored outline stayed hidden until a
real blur/refocus cycle - which is why toggling Plan/Act mode brought it
back. Stop clearing the flag on send; blur is already handled by the
onBlur handler.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-01 18:02:59 -07:00
Saoud Rizwan e450cbf5dd fix(vscode): settle pending tool approval when a message edit replaces the session (#12836)
Editing a previous message while a tool approval prompt was pending left the
old session's approval promise parked forever: the superseded run stayed
suspended awaiting an answer that could never come, and the stale resolver
kept intercepting later ask responses. Clear pending interactions before
starting the replacement session, exactly like cancelTask / clearTask /
task-switch / mode-change already do.

Ref #12827

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-01 18:02:01 -07:00
Saoud Rizwan 830ef5d288 Fix AskSage custom API URL being ignored at inference time (#12843)
The runtime baseUrlMap in resolveBaseUrl lacked the asksage ->
asksageApiUrl mapping (present in store.ts, effective-config.ts, and the
legacy migration), so a custom AskSage API URL saved in legacy state was
never read and requests fell through to the builtin default
https://api.asksage.ai/server.

Also write the URL through the SDK provider-config store in
AskSageProvider.tsx (mirroring AnthropicProvider) so providers.json
stays in sync for CLI/desktop hosts; the store mirrors baseUrl back to
the legacy asksageApiUrl state key, keeping the /get-models fetch and
legacy readers working.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-01 18:00:58 -07:00
Saoud Rizwan 723ee3b610 fix(settings): persist Qwen/Moonshot API line to providers.json (#12837)
Convert the Qwen and Moonshot regional API line dropdowns from
legacy-state-only writes to useProviderConfig().write({ apiLine }),
matching the Z AI pattern. The host store mirrors the write back to the
legacy qwenApiLine/moonshotApiLine state keys, so a single write keeps
providers.json (read by the CLI and desktop app) and the legacy
StateManager (read by the VS Code session factory) in sync.

Adds store tests pinning the dual-write mirroring and webview component
tests for the dropdowns' write and display behavior.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-01 18:00:16 -07:00
cline-cloud[bot] 75e1cc7a6f fix(settings): restore custom URL toggle after clear failure (#12838)
* fix(settings): restore custom URL toggle after clear failure

* fix(settings): cancel pending URL edit before clear

---------

Co-authored-by: Cline <noreply@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-08-01 17:55:54 -07:00
Sufiyan Khan 96f8fbf671 fix(terminal): complete commands on shell execution end (#12658) 2026-08-01 17:26:42 -07:00
Tran Binh Minh 211cc035bd fix(vscode): include untracked files in commit message generation (#12069)
* fix(vscode): include untracked files in commit message generation

getGitDiff only ran git diff --staged and git diff HEAD, neither of which reports untracked files, so an add-only working tree failed with 'No changes in workspace for commit message'. Gather untracked files and diff each against /dev/null via execFile (argv, no shell) so add-only trees work and special-char filenames are safe.

Closes #12060

* fix(vscode): include untracked files alongside tracked changes

Address review: append untracked-file diffs in the non-staged path instead of gating on an empty diff, so a mix of edited tracked files and new untracked files includes both. Re-throw git exit codes other than 1 (files differ) so real errors aren't swallowed. Use a named, non-runnable label for the output header. Adds a mixed tracked+untracked test.

Refs #12060

---------

Co-authored-by: Minhkunn <minh.12072k6@gmail.com>
2026-08-01 17:14:54 -07:00
Dominic Cooney 1e9e3c7a3e fix(cli): restore the formatDisplayUserInput import in root.tsx (#12844)
#12831 removed the import while rewriting checkpoint restore, and #12830
landed on top of it adding a usage that assumed the import was still
there. apps/cli typecheck has failed on main since, which fails the
sdk-test Quality Checks job on every PR touching sdk/**.

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-08-01 16:53:50 -07:00
Saoud Rizwan e543c692c7 fix(settings): persist custom base URL checkbox state and stop keystroke loss in URL fields (#12834)
* fix(settings): persist and display custom base URL checkbox, stop keystroke loss in URL fields

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(settings): drop stale provider-config responses and re-read after failed writes

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(settings): skip write-failure recovery read when a newer write is in flight

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-08-01 13:15:07 -07:00
Saoud Rizwan e34e794624 fix(cli): show plain text when prefilling a restored message (#12830)
After a checkpoint restore (/undo or Esc Esc), the rewound user message is
dropped into the input box to edit and re-send. It was prefilled from the
raw stored text, which the runtime wraps in a <user_input mode="...">
envelope, so the input showed '<user_input mode="act">...</user_input>'
instead of what the user typed. Prefill the display form via
formatDisplayUserInput (already used for the picker preview and imported in
this file), which strips the envelope and preserves slash-command display
form.
2026-08-01 10:01:16 -07:00
Saoud Rizwan 8d078f59bd fix(checkpoints): create reliably, full workspace rewind on restore, repair CLI /undo (#12831)
* fix(core): create checkpoints reliably across hosts, restarts, and compaction

#12691 moved checkpoint run-boundary detection into a beforeRun hook that
recorded snapshot.messages.length, assuming the run's user prompt is
appended afterwards. SessionRuntime (VS Code + CLI) instead seeds the
prompt into initialMessages and calls run(""), so the beforeRun delta is
always empty and no checkpoints were ever created in either surface.

Gate checkpoint creation on two signals instead of the fragile in-memory
delta alone:
- introducedUserRun: the beforeRun delta contains a new user turn. Covers
  hosts that pass the prompt as run input and refreshes the entry on
  edit-and-regenerate.
- alreadyCheckpointed: the run count already exists in the DURABLE session
  checkpoint history. Covers the seeded-prompt path and, unlike an
  in-memory counter, still holds after a process restart.
Skip only when neither applies (a continuation/resumption re-running an
already-checkpointed run), so a reopened session can't overwrite a good
pre-run snapshot with the mutated workspace. Run numbering uses the
span-aware countUserRunMessages so it survives compaction folding turns
into one summary message.

Adds regression tests for the seeded-prompt creation, the reopen-without-
new-turn overwrite case, and the first-turn-after-compaction case.

* fix(cli): number /undo checkpoints span-aware so restore can map them

The interactive /undo picker counted every role="user" message when
assigning run numbers to checkpoints. Tool-result messages also carry
role "user", so any turn that used tools got an inflated run number; the
picker then handed that number to the core, whose span-aware
findUserRunMessage could not map it and aborted with 'Could not find user
message for run N'. Restore was effectively unusable whenever the agent
called a tool.

Count runs with the core's getUserRunSpan (tool results contribute 0, a
compaction summary spans the turns it folded) so the picker's run numbers
match what the core records and resolves. Extracted the item-building into
a pure buildCheckpointPickerItems helper with unit coverage for the
tool-result and compaction cases.

* fix(core): capture untracked files in checkpoints as a third parent

Checkpoint creation used plain `git stash create`, which cannot include
untracked files (no -u support). Restore therefore had no way to bring back
a file Cline created during a task, so a full rewind was impossible.

Synthesize a stash-shaped snapshot commit that also records untracked,
non-ignored files as a third parent - exactly like
`git stash create --include-untracked` - without touching the working tree,
the real index, or the stash list: list `ls-files --others
--exclude-standard`, stage into a temp GIT_INDEX_FILE, write-tree +
commit-tree to get the untracked parent, then rebuild the stash commit with
that extra parent. When the tracked worktree is clean but untracked files
exist, synthesize the stash from HEAD so they are still captured instead of
falling back to a bare HEAD-commit checkpoint. Fully clean worktrees still
use the HEAD-commit fallback.

* fix(core): full workspace rewind on restore for snapshot checkpoints

Restore now rewinds untracked files generation-aware:
- If the checkpoint carries an untracked third parent (a snapshot from
  createWorktreeStashCommit), do a full rewind: reset tracked to the base,
  `git clean -fd` to drop files created after the checkpoint (and clear the
  worktree so `stash apply` cannot hit an "already exists" conflict), then
  `git stash apply`, which restores each captured untracked file to its
  checkpoint-time content from the third parent. `git clean -fd` (no -x)
  leaves .gitignored paths - build output, node_modules, .env - alone. This
  is safe because everything removed is either recreated from ^3 or postdates
  the checkpoint, and the pre-restore recovery snapshot (stash push
  --include-untracked) can roll the whole operation back.
- If the checkpoint has no third parent (legacy 2-parent stashes and
  HEAD-commit fallbacks from before capture existed), keep the conservative
  behavior: never touch untracked files, since nothing can reconstruct them.

This makes 'Reset Code' / '/undo' a true rewind: a file Cline created in an
early turn and ruined later comes back to the early-turn version.
2026-08-01 10:00:33 -07:00
cline-cloud[bot] 94f897f559 fix(telemetry): preserve provider error details (#12824)
Co-authored-by: Cline Cloud Agent <cline-cloud-agent@users.noreply.github.com>
2026-08-01 10:06:02 +02:00
Saoud Rizwan 4b529f5f81 fix(telemetry): stop counting tool use mistake notices as provider API errors (and stop double-counting them) (#12820)
* fix(telemetry): single classified emitter for provider API errors, gated on terminal failures

* chore: remove explanatory comment block from agent-events.ts

* feat(telemetry): stamp terminal=true on SDK provider failure events

* chore: remove dead notice api_error capture (no producer emits that reason)

* refactor(telemetry): rename provider-failure 'terminal' flag to 'fatal' (terminal is the shell in Cline)

* refactor(telemetry): drop the fatal flag - only user-surfaced failures are reported on both bundles
2026-07-31 23:53:58 -07:00
Tomás Barreiro 978155814e Fix Text rendering when restarting ACP sessions (#12823) 2026-07-31 23:48:41 -07:00
Saoud Rizwan 055210a2bf fix(cli): don't let the ClinePass promo dialog trap users whose terminal drops Esc (#12819)
* fix(cli): don't let the ClinePass promo dialog trap users whose terminal drops Esc

The promo dialog could only be dismissed with Escape, and Esc is the
least reliable key across terminals: it arrives as a bare \x1b that
needs timeout disambiguation, and Bun's Windows console input layer is
known to swallow it (Windows PowerShell users reported being unable to
dismiss the dialog at all). Worse, the 'shown' marker was only written
when the dialog closed, so a user who force-quit saw the promo again on
every launch.

- Any key other than Enter now dismisses the dialog (Enter still opens
  the subscription page)
- The shown marker is persisted when the dialog is displayed, not when
  it is dismissed, so a force-quit never loops the promo
- Add a tuistory e2e test covering marker timing and any-key dismissal

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): let any key cancel the OAuth waiting screen

Like the ClinePass promo, the OAuth wait screen was dismissible only
with Esc (plus K for the API-key fallback when offered) while blocking
on a browser flow that may never complete — a trap on terminals that
drop Esc. Any key other than K now cancels the pending auth attempt;
K still switches to manual API key entry when available.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): don't let a modifier keypress dismiss link-bearing dialogs

The ClinePass promo and OAuth wait screens both render a URL the user
opens by holding Cmd/Ctrl and clicking. With 'any key closes', that
modifier keystroke could tear the dialog out from under the click. Add
a shared isAnyKeyDismiss() guard so only unmodified keys dismiss; keys
held with ctrl/meta/super/hyper (and bare modifier presses) are ignored.
Enter still opens the promo and K still opens manual API key entry.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert(cli): persist promo shown-marker on dismiss again

Now that any key dismisses the promo, users can reliably close it, so
there's no need to write the shown-marker eagerly on display. Restore
persisting it in the dialog's finally() and update the e2e assertion.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-31 23:44:29 -07:00
Saoud Rizwan 8e68b14673 chore(sdk): release v0.0.68 2026-07-31 22:15:38 -07:00
Saoud Rizwan 4061dda034 fix(cli): close remaining open-package gaps in browser URL opening (#12822)
Follow-up to #12782, which replaced the open package with openUrlInBrowser
but missed two call sites and dropped some platform handling the package
provided:

- Migrate the two remaining open users (skills marketplace open in
  tui/root.tsx and ACP OAuth in acp/auth.ts) to openUrlInBrowser; the
  listenerless-child crash fixed by #12782 was still reachable there.
- Treat containers running on a WSL2 kernel (Docker Desktop for Windows,
  devcontainers) as plain Linux: /proc/version says microsoft but there is
  no Windows interop, so use xdg-open instead of powershell.exe (matches
  the is-inside-container check open@10 performed).
- Try opener candidates in order: on WSL, powershell.exe on PATH, then the
  absolute /mnt/c/... path (covers appendWindowsPath=false), then xdg-open
  (sandboxed WSL with WSLg); on win32, the %SystemRoot% absolute PowerShell
  path first (what open@10 used), then PATH lookup.
- Convert Linux file paths to \\wsl$ UNC paths via wslpath before handing
  them to Start-Process, so 'cline doctor log' works on WSL.
- Remove the now-unused open dependency from apps/cli.
2026-07-31 22:07:36 -07:00
Dominic Cooney b93a8cd442 fix(vscode): run Store PowerShell profiles correctly (#12802)
* fix(vscode): run Store PowerShell profiles correctly

* test(vscode): cover Store PowerShell background execution

* fix(core): handle shell stdin write failures

* test(windows): verify legacy PowerShell execution
2026-07-31 22:03:10 -07:00
Saoud Rizwan 72561771a7 docs: move ACP editor integration to a dedicated Usage page (#12821) 2026-07-31 21:26:14 -07:00
Mikołaj Kondratek 123477dcd9 chore: remove dead host-side capture methods for core-owned telemetry events (#12818)
captureDiffEditFailure and captureWorkspaceInitError have no callers: SDK core
is the sole emitter of task.diff_edit_failed and workspace.init_error. Keeping
callable host-side capture APIs for core-owned events is how the
task.provider_api_error double-emission happened — a future host caller would
silently double-count these events with no type error or failing test. Also
drops the two event-name constants, which were only referenced by the removed
methods.
2026-07-31 21:03:37 -07:00
Saoud Rizwan 2ac20c647c docs: add Editor Integration (ACP) section to CLI overview (#12808)
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-31 20:52:37 -07:00
Saoud Rizwan 316efc4521 Resolve recommended-model display names once in the SDK feed (#12806)
* feat(core): resolve display-ready names in fetchClineRecommendedModels

* refactor(cli,vscode): render recommended-model names from the enriched feed

* fix(core): resolve catalog names through vercel/openrouter id aliases

* fix(core): share one timeout budget across the feed and catalog lookups

Greptile flagged that resolveDisplayNames started a fresh timeoutMs window
after the recommendation request finished, so a slow endpoint plus a cold
or hung catalog could keep the picker loading for ~2x the timeout. The
catalog race now gets only the budget remaining from a single deadline;
an already-cached catalog still applies on an exhausted budget because
its promise resolves ahead of the zero-delay timer.
2026-07-31 20:49:59 -07:00
Saoud Rizwan b0ee2cc80a ci(vscode): publish combined stable VSIX to Open VSX and harden ab-package gates (#12805)
* ci(vscode): publish combined stable VSIX to Open VSX and harden ab-package gates

* ci(vscode): address greptile review — env-routed version input, bookkeeping survives Open VSX failure
2026-07-31 20:09:09 -07:00
Cline Test a063317218 fix(connectors): strip the Slack bot mention from incoming messages (#12780)
* fix(cli): strip the Slack bot mention from incoming connector messages

Slack delivers an at-mention of the app as `<@U0B8E8H3U1F> hi`, and the chat
SDK deliberately leaves the bot's own mention unresolved so mention detection
keeps working - flattening it to `@U0B8E8H3U1F hi`. The connector forwarded
that verbatim, so the agent saw the raw bot id at the front of every
mention-triggered turn.

Strip the leading self-mention in onNewMention/onSubscribedMessage before the
approval-reply check and handleTurn, resolving the bot id from the adapter
(request-scoped in multi-workspace mode) with a fallback to the event envelope
authorizations. Mentions of other users and inline mentions are preserved, and
a bare mention is left as-is so the turn is not dropped as empty input.

* fix(cli): only strip a complete Slack bot mention, not an id prefix

The `<@ID>` and `<@ID|name>` alternatives in stripSlackBotMention are
terminated by `>`, but the SDK-flattened bare `@ID` alternative had no
trailing boundary, so it also matched the start of a longer id. With bot id
`U123`, a message addressed to a different user - `@U1234 help` - was
rewritten to `4 help`, corrupting both the approval-reply check and the text
handed to the agent.

Require the flattened alternative to be followed by a non-id character with a
`(?![A-Za-z0-9])` lookahead, so it only matches a complete Slack id. A plain
`\b` cannot express this, because Slack ids end in word characters and `\b`
still matches between `U123` and `4`.

Existing behaviour is unchanged: angle-bracket and flattened self-mentions are
still stripped, repeated leading mentions still collapse, trailing `[\s,:]`
separators are still consumed, other users' and inline mentions are preserved,
and a bare mention is still left untouched so the turn is not dropped as empty.

Adds regression tests for the prefix collision, which fail against the previous
regex and pass with this one.

---------

Co-authored-by: cline-test-bot <cline-test-bot@users.noreply.github.com>
2026-07-31 19:31:04 -07:00
Tomás Barreiro 6d10f363b3 Add CLinePass as a provider on ACP (#12793)
* Add CLinePass as a provider on ACP

* Resolve default model id
2026-08-01 03:28:37 +02:00
Mikołaj Kondratek ab0bc93182 fix: surface upstream provider error from gateway-forwarded stream failures (#12800)
* fix: surface upstream provider error from gateway-forwarded stream failures

Vercel AI Gateway streams upstream rejections (e.g. Alibaba Qwen context-
length errors) wrapped in its own parse failure: the top-level message is
just 'Stream error occurred' and the cause is an internal ZodError, while
the real rejection is JSON-encoded in value.error_message. Unwrap it so
users see 'This model's maximum context length is 40960 tokens...' instead
of a raw Zod issue dump.

Also fall back to JSON.stringify for opaque object errors so the UI never
renders '[object Object]'.

* refactor(llms): use shared safe-JSON helpers and a named type guard in extractErrorMessage
2026-07-31 18:08:32 -07:00
Saoud Rizwan 22ded04bc4 Resolve display names for Cline free models in the CLI and extension model pickers (#12801)
* fix(llms): resolve OpenRouter display names for all Cline free models

* fix(vscode): resolve featured model card display names from the provider catalog

* fix(vscode): fall back to endpoint-provided names on featured model cards
2026-07-31 17:25:16 -07:00
Saoud Rizwan edaab58716 Add tuistory-based TUI e2e harness for the CLI (#12796)
* Add tuistory-based TUI e2e harness for the CLI

Evaluates https://github.com/remorses/tuistory as a Playwright-style
driver for the interactive TUI. Adds:

- tuistory devDependency in apps/cli
- test:e2e:tuistory script + vitest.tuistory.e2e.config.ts
- src/cli.tuistory.e2e.test.ts: ports the script(1)-based interactive
  smoke tests to reactive waitForText/screen-state assertions against a
  real PTY + Ghostty terminal emulator (5 tests, ~11s, no fixed sleeps)
- DEVELOPMENT.md docs for the vitest suite and the tuistory session CLI
  agents can use to manually drive the TUI headlessly

* Add tuistory agent skill (.cline/skills, symlinked to .claude/.agents)

Teaches coding agents to drive the Cline TUI headlessly via tuistory
sessions (launch with isolated env, reactive wait, snapshot/screenshot,
observe-act-observe loop) and to write launchTerminal()-based e2e tests,
closing the loop for cloud agents testing apps/cli.
2026-07-31 17:14:22 -07:00
Tomás Barreiro 0ec999b31a Remove CLI Promo Code (#12797)
* Remove CLI Promo Code

* fix import

* fix tests

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-31 16:52:58 -07:00
Octopus 3845be53b3 fix(llms): preserve video input capability (#12787)
Co-authored-by: octo-patch <266937838+octo-patch@users.noreply.github.com>
2026-07-31 16:39:50 -07:00
Saoud Rizwan 8014a05088 Revert "fix(cli): don't crash when no browser opener binary exists (#12782)" (#12799)
This reverts commit c7850423f1.
2026-07-31 15:58:03 -07:00
Saoud Rizwan c7850423f1 fix(cli): don't crash when no browser opener binary exists (#12782)
* fix(cli): don't crash on browser-open failure when no opener binary exists

open() with { wait: false } resolves to the detached child process before
the opener binary is known to exist. On hosts without one (e.g. xdg-open
on headless Linux), the failure arrives as an async 'error' event on the
listenerless child, escalating to an uncaughtException that kills the CLI
— bypassing every try/catch and .catch() at the call sites. Hitting
"Sign in with Cline" from the welcome screen reliably crashed the TUI in
containers.

Route all browser opens through a shared openUrlInBrowser() helper that
attaches the error listener and reports failure via its returned promise,
so flows fall back to their existing "visit the URL below" messaging.

* fix(cli): attach opener error listeners in the same tick as spawn

Greptile's review caught that the helper attached its listeners only after
awaiting open()'s promise. Empirically that window is safe under Node 22
(the listener wins) but real under Bun — the runtime the compiled CLI
ships on — where the missing-binary ENOENT 'error' event fires before the
microtask queue drains, reproducing the exact crash this helper exists to
prevent.

macOS and non-WSL Linux now spawn their opener (open / xdg-open) directly
with listeners attached in the same synchronous tick, which both runtimes
guarantee can never miss the event. Windows and WSL keep delegating to the
open package for its shell quoting and interop routing; their openers
(cmd/powershell) always exist, so the post-await path cannot hit ENOENT.
The regression test now emits the error on nextTick — before microtasks —
which fails against the previous implementation.

* fix(cli): drop the open package — same-tick opener spawn on every platform

The win32/WSL delegate path still attached listeners after awaiting
open()'s promise, leaving a narrow uncaught-error window under Bun for
emittable spawn failures (e.g. AV-blocked EPERM). Spawn the opener
directly everywhere instead: open on macOS, xdg-open on Linux, and
powershell -EncodedCommand on Windows/WSL — the base64-encoded
Start-Process command sidesteps cmd/PowerShell quoting of URLs entirely,
so nothing is ever shell-interpolated.
2026-07-31 14:29:14 -07:00
Saoud Rizwan ed5f3031b0 fix(cli): silence dialog-container 'not a child of __root__' warning on exit (#12795)
On CLI exit, renderer.destroy() runs root.destroyRecursively() before
React flushes the DialogProvider's passive unmount cleanup, so the
dialog container is already detached when the cleanup calls
renderer.root.remove(container), triggering OpenTUI's 'Renderable with
id dialog-container is not a child of __root__, skipping remove'
warning. Drop the explicit remove from the patched @opentui-ui/dialog
provider cleanup (react + solid): Renderable.destroy() already detaches
from its parent when attached and no-ops when already destroyed.
2026-07-31 14:23:17 -07:00
Tomás Barreiro 3f38bd516f Add Organization selector to ACP (#12774)
* Add Organization selector to ACP

* linter
2026-07-31 12:01:46 -07:00
Tomás Barreiro be8c16b0ae Fix ACP session resolution (#12756)
* Fix ACP session resolution

* Use the selected provider/model

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-31 10:09:46 -07:00
Sufiyan Khan 0074b7222f fix(vscode): remove attachments from edited messages (#12578) 2026-07-31 10:05:30 -07:00
Mikołaj Kondratek ce81bc6855 fix(core): omit workspace hint for filesystem root paths (#12778)
basename("/") is an empty string, which WorkspaceInfoSchema rejects
(hint is z.string().min(1).optional()), so upsertWorkspaceInfo threw a
ZodError for any session rooted at the filesystem root — e.g. the
desktop app launched from the Dock with cwd "/" — and commands never
ran. Omit the hint instead of storing an empty string.
2026-07-31 17:50:46 +02:00
Bee e0803124b2 fix(cli): restart hub via installed wrapper after update (#12755)
Launch the hub through CLINE_WRAPPER_PATH after Unix self-updates so npm 12 does not reuse a deleted cached executable. Preserve the in-process fallback for Windows and development builds, and add coverage for success and failure paths.

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-31 01:01:22 -07:00
Saoud Rizwan 644e841737 chore(vscode): bump to 4.1.2 for release 2026-07-30 22:02:40 -07:00
Saoud Rizwan 5c6753e6d7 Show Legacy/Next extension variant in the settings About page (#12777) 2026-07-30 21:57:04 -07:00
Saoud Rizwan fdcc5367dc chore(vscode): bump to 4.1.1 for release 2026-07-30 21:08:32 -07:00
Dominic Cooney 901fdbc5cb Remove vestigial MCP server-key machinery from McpHub (#12773)
The uid/mcpServerKeys registry existed to encode server names into
native tool-call function names and decode them back at dispatch.
That encode/decode path was removed with the extension host
(c4c126bee): tool names are now built by the SDK's deterministic
defaultMcpToolNameTransform and execution closes over the server
name directly, so getMcpServerByKey has no callers and the keys are
write-only state. Delete the registry, the uid field, and the
deleteServerKey callback plumbing.

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-07-30 21:05:58 -07:00
Saoud Rizwan 69e3149f5f ci(vscode): add tag, GitHub Release, and Slack bookkeeping to combined publish workflow 2026-07-30 20:59:08 -07:00
Saoud Rizwan 3a3d0c1bc3 chore(vscode): bump to 4.1.0 and backport legacy 4.0.x changelog to main 2026-07-30 20:59:08 -07:00
Tomás Barreiro 0746ea72bf Improve ACP agent errors (#12766)
* handle finish reasons

* describe agent error

* use SDK functions

* Add isLikelyAuthError to the check

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-30 19:12:38 -07:00
Sebastien Tardif f0d5ede555 fix: replace flaky setTimeout waits with drainForTesting in BannerService tests (#10530)
BannerService tests still used 10ms sleeps for background fetch completion.
On slow CI runners that races mocha timeouts. drainForTesting() already
exists and awaits the in-flight fetch promise deterministically.

Rebased onto monorepo main (apps/vscode path).

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
2026-07-30 18:24:00 -07:00
Saoud Rizwan ed821a6456 chore(cli): release v3.0.48 2026-07-30 18:17:43 -07:00
Saoud Rizwan f36b59c9cb chore(sdk): release v0.0.67 2026-07-30 18:03:17 -07:00
Saoud Rizwan 311959e757 ci(vscode): add test gate to combined A/B publish + publish-extension skill (#12764)
* ci(vscode): gate the combined A/B package workflow on both bundles' test suites

* docs(skills): add publish-extension skill for VS Code extension releases

* ci(vscode): pin tested revision for next bundle and refuse publishing untested next-refs

* ci(vscode): pin legacy bundle to the revision its test gate ran against
2026-07-30 17:54:29 -07:00
Saoud Rizwan f21e6faf1c fix(vscode): show migrated model in settings instead of hardcoded default (#12768)
* fix(vscode): show migrated model in settings instead of hardcoded default

After the SDK provider migration a user who never explicitly picked a model
(i.e. took the legacy default) ends up with the model recorded in
providers.json but not in the mode-specific globalState fields the settings
picker reads. The OpenRouter picker and its info card then fell back to the
hardcoded openRouterDefaultModelId (claude-sonnet-4.5) and its pricing, while
the extension actually ran the migrated model (claude-sonnet-5).

- resolveModelInfo: when no model id is requested, honor the provider store's
  committed selection (which reads providers.json when the state field is
  empty) before substituting a catalog default.
- OpenRouterModelPicker: source the displayed model id/info from the
  authoritative resolver as the fallback when the mode fields are empty,
  instead of the hardcoded constant. Committed-field users are unaffected.

* fix(vscode): guard picker model info against resolver default substitution

Review hardening: the resolver substitutes its provider default for ids it
cannot resolve, so only trust its info when it answered for the id actually
displayed. Prefer the live catalog entry for the displayed id (synchronous
once fetched, which also removes the transient placeholder while the resolver
is in flight), and never render another model's metadata under the displayed
model's name. Also document why the act-then-plan readSelection order in the
empty-id branch cannot misattribute a mode-specific selection.
2026-07-30 17:51:21 -07:00
Saoud Rizwan 1cf19304f4 docs: restore 'open Cline in right sidebar' guide as section of IDE usage page (#12771) 2026-07-30 17:43:58 -07:00
Saoud Rizwan 09aec528d0 Fix task export button: resolve the SDK session folder reliably (#12772)
* Restore task export to markdown and show download button in all builds

* Render untyped tool outputs and object tool inputs as JSON in task export

* Open the task's SDK session folder from the export button and show it in all builds

* Keep the task header session-folder button dev-only
2026-07-30 17:34:29 -07:00
Saoud Rizwan 4ec2b68c7c Fix queued prompt row alignment and auto-scroll when queueing a message (#12767)
* Fix queued prompt row alignment and auto-scroll on queue

Center the dot, badges, and cancel button on the first text line of each queued prompt row (the X previously sat ~3px below the text), and re-pin the chat view to the bottom when a prompt is queued so the queue banner doesn't cover the end of the conversation.

* Don't treat task switches as queue growth for auto-scroll

Guard the queued-prompt auto-scroll effect on the displayed task's ts: switching to a task that already has queued prompts grows the count without a send from this webview, and should not hijack the newly opened conversation's scroll position.
2026-07-30 17:33:19 -07:00
Saoud Rizwan 56fd6bb1ce Fix hidden plan/act mode-switch prompts reappearing when resuming a task from history (#12769)
* fix(vscode): hide synthetic mode-switch and resumption prompts when rehydrating chat from history

* chore: add changeset
2026-07-30 17:04:32 -07:00
Tomás Barreiro 49c7a89882 Inject device_id into tracking events (#12708)
* Inject  into tracking events

* fix sandbox resolution
2026-07-31 01:13:17 +02:00
Bee b47851791c feat(desktop): support message editing & checkpoints (#12691)
* feat(desktop): support message editing & checkpoints

Fork sessions before a selected user run, trim checkpoint history, and restore prior messages so prompts can be edited safely. Update the chat UI and tool activity panels to support the editing flow and preserve horizontal scrolling for long content.

* fix(desktop): restore checkpoints when editing messages

* fix(core): infer kindless checkpoint types

* fix(core): preserve checkpoint run numbering

* fix(desktop): make message edit restores transactional

* fix(desktop): make checkpoint restores workspace-atomic
2026-07-30 15:50:33 -07:00
Saoud Rizwan c0a966c46a Restyle compact-task confirmation as a bordered card with even spacing (#12759)
The confirmation that appears when clicking the compact button in the
task header was a bare unstyled row with a stray bottom margin (my-2)
that stacked on the header card's own bottom padding, leaving a dead
gap under the buttons. It is now a distinct bordered card (editor
background against the header's toolbar surface) with a title, a short
description of what compacting does, and right-aligned Cancel/Compact
buttons, with symmetric spacing above and below.

Also drops the ContextWindow wrapper's bottom margin (my-1.5 -> mt-1.5)
so the row's bottom spacing matches the header padding, and adds a
ContextWindow Storybook story that mirrors the expanded TaskHeader
surface so the confirmation can be previewed in isolation.
2026-07-30 15:42:30 -07:00
Saoud Rizwan cf3f3e08eb fix: don't block provider switch UI on ClinePass account switch (#12758)
Selecting ClinePass in settings awaited a network round-trip (PUT
/active-account + possible token refresh) before postStateToWebview,
so the settings panel stayed on the previous provider until the
request finished. Make the personal-account switch fire-and-forget:
it was already best-effort, and auth state changes propagate to the
webview separately once it completes.

Also convert the helper's test to bun:test so it actually runs (the
mocha version was excluded by both the bun unit runner and the
vscode-test glob) and fix its stale null-vs-undefined assertion from
the SDK migration.
2026-07-30 15:42:06 -07:00
Ara 7712e44468 fix(vscode): show catalog-driven reasoning effort selector for xAI, Z AI, and Moonshot (#12754)
The xAI, Z AI, and Moonshot settings components were never wired to the
catalog's reasoning capability: xAI only offered a legacy low/high
checkbox hardcoded to grok-3-mini model ids, and Z AI / Moonshot had no
reasoning control at all, even though models.dev marks grok-4.5, glm-5,
kimi-k2-thinking, etc. as reasoning models. Every catalog-driven
provider (GenericProviderSettings, OpenRouter/Vercel/Requesty pickers)
already gates ReasoningEffortSelector on supportsReasoning.

Render the shared ReasoningEffortSelector in these three components when
the selected model's catalog info advertises reasoning, persisting the
choice to the provider config the same way GenericProviderSettings does.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-30 14:39:48 -07:00
Saoud Rizwan f77f584a32 fix(vscode): stop queued-prompt turns from getting stuck on Thinking (#12751)
* fix(vscode): stop queued-prompt turns from getting stuck on Thinking

When the SDK drains a queued prompt at the end of a turn, the new turn's
pending_prompt_submitted bookkeeping (isRunning=true, phase=streaming) always
runs before the previous turn's send promise unwinds in fireAndForgetSend.
That .then then unconditionally called setRunning(false), so the queued turn
ran with isRunning=false and its own turn-complete was mistaken for a
cancelled-turn straggler - the phase never left "streaming" and the chat
showed an endless Thinking indicator.

Track a monotonic turn epoch on SdkSessionLifecycle: immediate sends and
drained queue prompts bump it, and both the send-settled callbacks and the
event coordinator's turn-end handling skip their bookkeeping when a newer
turn has started since (covers the symmetric interleaving where the done
handler resumes after the drain and would clobber the queued turn's
streaming phase).

* Simplify: preserve only an actual cancel phase in the turn-complete straggler guard

Replaces the turn-epoch machinery with the minimal fix: the straggler
guard's intent is to preserve the cancel-set "resumable" phase, so key it
on the phase itself instead of the isRunning proxy. When the SDK drains a
queued prompt at turn end, the previous turn's send promise settles after
the queued turn already started and flips isRunning back to false
mid-turn; with the old guard the queued turn's real completion was then
mistaken for a cancel straggler and the phase stayed stuck on
"streaming" (endless Thinking). Checking for "resumable" lets that
completion resolve the terminal phase normally while cancel behavior is
unchanged.
2026-07-30 14:35:33 -07:00
Saoud Rizwan 2771760305 fix(vscode): stop thinking loader flickering around mid-turn tool calls (#12750)
The anti-flash grace period (added to stop the loader flashing at turn
end) also fired mid-turn, causing a visible hide/show/hide flicker right
before a tool row appeared:

- When a reasoning tail finalized while the turn kept streaming, the
  reasoning shimmer collapsed, the loader stayed hidden for the 500ms
  grace, popped in, then hid again when the tool row landed. Reasoning
  never ends a turn, so skip the grace for reasoning tails and hand the
  shimmer straight to the loader.
- When the loader was already visible below a streaming tool group, the
  group tail finalizing blinked it off for the grace period. The grace
  now only delays hidden -> visible transitions, never hides an
  already-visible loader.
2026-07-30 14:32:15 -07:00
Saoud Rizwan 16d0d04573 Show user message immediately when sending to a task opened from history (#12753)
* Show user message immediately when sending to a history-resumed task

Sending a message to a task opened from history routed through the
resume_task/resume_completed_task askResponse branch, which forced the
Thinking loader but never set the optimistic user_feedback bubble. The
extension only echoes the user's message after the full SDK session
resume completes, so the chat showed a Thinking indicator with no user
message until the (slow) resume finished.

Pass showPendingMessage on the resume branch like the other
non-streaming follow-up paths, so the user's message appears in the
chat immediately. The optimistic bubble reconciles with the extension's
say:user_feedback echo once the resume completes (identical raw text).

* Add changeset
2026-07-30 14:28:18 -07:00
Saoud Rizwan 3590b425eb fix(ci): de-flake Windows bun unit tests (hook PowerShell bridge + runner retry) (#12752) 2026-07-30 14:21:05 -07:00
Bee 12404f0a4b fix(core): add a plugin telemetry bridge (#12741)
* fix(core): add a plugin telemetry bridge

* fix(core): address plugin telemetry bridge review feedback

- Sanitization fallback now covers the whole executeTool IPC payload:
  `input` can be rewritten by beforeTool hooks or programmatic callers,
  so a non-serializable input degrades gracefully like the context does.
- The sandbox only offers ctx.telemetry when the host actually has a
  telemetry service (new PluginSandboxOptions.telemetryAvailable, derived
  from options.telemetry in the config loader), so feature-detecting
  ctx.telemetry means "someone is listening" in both execution modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(core): plugin telemetry review round 2 — timer leak and setup-time fallback

- SubprocessSandbox.call: a synchronous child.send() throw (cyclic payload)
  left the pending timeout timer armed; it later fired and shut the sandbox
  down, killing unrelated in-flight calls. Cancel the pending entry and
  reject with the original error so serialization failures stay classifiable.
- plugin_telemetry events emitted during plugin setup() arrive before the
  session is registered, so the session-config lookup missed and setup-time
  telemetry was silently dropped. Route through a fallback telemetry service
  (extensionContext/local config/host default), mirroring handlePluginLog's
  fallback logger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(core): classify BigInt IPC serialization errors for the sandbox fallback

Bun ("cannot serialize BigInt") and Node ("Do not know how to serialize a
BigInt") raise messages that did not match the cyclic/circular predicate, so
a bigint smuggled into tool input or context by a hook or programmatic caller
rethrew instead of retrying with the JSON-safe clone — which already drops
bigint leaves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 13:44:25 -07:00
Saoud Rizwan 6cb43f0309 fix(core): make compaction sidecar persistence reliable (#12747)
Auto-compaction state was silently rejected on every save ("Skipped
stale session compaction state"), forcing a full re-compaction — an
extra summarizer LLM call — on every turn past the trigger, and a
resume-time identity churn could leave a dead sidecar permanently
blocking replacements.

Three changes:

1. Stop hashing volatile transport identity. The source-prefix hash no
   longer includes message id/ts, which the codec regenerates on every
   wire/storage round-trip (a store's just-appended user turn has none
   yet; consolidated parallel tool results are re-split with minted ids
   on resume). The fingerprint now covers role, content, and durable
   metadata. Hash seed bumped to v2; v1 sidecars fail projection once
   and are replaced by the next compaction.

2. Validate persists against the exact source messages the state was
   computed over. createCompactionStateAwarePrepareTurn passes
   context.messages to saveState, and the local runtime host threads
   them into persistActiveSessionCompactionState instead of falling
   back to the conversation store's mid-turn shape.

3. Scope the count-based stale-write guard to states that still
   project. An unprojectable current state no longer blocks a
   newer-timestamped replacement, so invalidated sidecars self-heal
   instead of deadlocking the session.

All three regression tests fail on main and pass with this change.
2026-07-30 13:05:47 -07:00
Saoud Rizwan 3058563f37 fix(vscode): mark onboarding complete only after OAuth succeeds (#12744)
* fix(vscode): mark onboarding complete only after OAuth succeeds

The onboarding webview marked welcomeViewCompleted immediately after the
sign-in URL opened (accountLoginClicked resolves at URL-open time), so
Free/Frontier/ClinePass signups landed in chat signed out when the user
abandoned or failed browser auth, and the flag persisted across reloads.

Restore the classic extension behavior: the host (SdkAuthService) now
sets welcomeViewCompleted after the OAuth token exchange succeeds, in
createAuthRequest, handleAuthCallback, and the E2E mock login. The
webview persists the model selection up front, stays on the 'Almost
there!' step until auth completes, and fires the 'completed' funnel
event via a pending-intent module once clineUser arrives (mirroring the
pendingClinePassSubscribe pattern). This also fixes the legacy
WelcomeView fallback, whose 'Get Started for Free' never completed
onboarding after login.

* refactor(vscode): slim the onboarding-completion fix to its essentials

Drop the pending-telemetry module and App hook (the 'completed' funnel
event keeps its existing main-branch semantics, firing when the flow is
initiated, so no telemetry change in this PR), restore finishOnboarding
to its original shape with just a markCompleted parameter, and reduce
the host helper to a single setGlobalState call.
2026-07-30 12:40:44 -07:00
Saoud Rizwan 39de7479b8 Fix delayed and stale Plan/Act mode switches (#12732)
* fix(vscode): make plan act switches responsive

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): recognize completed plans with trailing usage

* fix(vscode): continue reopened completed plans

* fix(vscode): always publish state after mode rebuild

* fix(vscode): roll mode back when session replacement is refused

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-30 12:34:29 -07:00
Saoud Rizwan 86c2600a8a Show the thinking indicator immediately when starting a turn (#12746)
* Post streaming turn state to webview before session startup

The webview only learns the turn phase through full state posts, and the
first post after initTask happened only after startNewSession settled —
so the chat mounted with a stale idle TurnState and the thinking
indicator popped in noticeably late. Ship a state post right after the
initial task message is emitted, in parallel with session startup.

* Show thinking indicator optimistically on new-task submit

Capture the TurnState seq at the moment the newTask RPC is sent and
force the in-list Thinking loader row until a fresher TurnState arrives
(any phase), so the indicator renders together with the task message
instead of waiting for the streaming TurnState to round-trip. Rolled
back if the RPC fails; legacy (no turnState) hands off to the existing
tail heuristic once the task message lands.

* Paint the initial Thinking loader without waiting for Virtuoso

Frame-by-frame measurement showed the loader decision was true on the
chat view's first paint, but the synthetic in-list row still appeared
~150-200ms later: a cold-mounting virtualized list needs several frames
to measure and paint its first item. When the list has no visible rows
yet (new task just submitted), render the waiting row as a plain
element over the (empty) list instead; once any real row exists the
warm list takes over with the in-list row as before.

* Show thinking indicator immediately for follow-up messages too

Follow-ups had the same delay as new tasks: SdkController.askResponse
moves the phase to streaming but never posted state, so the webview
kept the stale terminal phase (hiding the loader) until the new turn's
first session event posted state. Post right after the phase change,
and generalize the webview's optimistic marker from new-task-only to
any turn-starting send (askResponse outside a streaming phase), with a
guard that never shows the loader while a content row is actively
streaming. Renames pendingNewTaskSeq to pendingTurnStartSeq.

* fix(vscode): render thinking loader synchronously
2026-07-30 12:28:40 -07:00
Saoud Rizwan f4230e475b fix(vscode): /compact UX — clear input, wrap divider, always update context header (#12735)
* fix(vscode): clear chat input immediately when /compact is submitted

* fix(vscode): let the compaction divider label wrap at narrow widths

* fix(vscode): update context-window header even when compaction grows the context

* chore: add changeset for /compact UX fixes

* docs(vscode): align getLastApiReqTotalTokens return doc with unclamped rescale
2026-07-30 12:13:33 -07:00
Saoud Rizwan 078abcd055 fix(ci): build shared package before the ui-publish desktop chat test
The desktop chat integration test renders components from @cline/ui, but
it also pulls @cline/shared/browser through the desktop app's own
message-content module. That subpath resolves to dist output no step in
this job produced, so the suite failed to collect.

Build @cline/shared before the test, and install the full workspace: the
two-package filter did not provide enough of the tree for that build.
2026-07-30 12:02:42 -07:00
Saoud Rizwan afc1229ab0 fix(ui): declare bun and node type dependencies
The ui-publish workflow installs only the @cline/ui and @cline/code
workspaces, so the root devDependencies that previously supplied the
'bun' and 'node' type roots were absent and tsc failed with TS2688.
Declare them on the package that requires them in its tsconfig types.

Also refreshes the stale @cline/code version recorded in bun.lock.
2026-07-30 12:02:42 -07:00
Saoud Rizwan 892837d352 Enable Auto Compact by default in VS Code (#12739)
* feat(vscode): enable Auto Compact by default

The SDK-based extension has no fallback context management: with auto
compact off, hitting the model's context window fails the request with a
provider error and retrying keeps failing (the legacy extension truncated
the oldest half of the conversation in this situation). The CLI already
defaults compaction on (agentic); align the extension with it.

* chore: add changeset for Auto Compact default-on
2026-07-30 12:02:02 -07:00
Ara 078d9f63f0 fix(models): tolerate null contextWindow/maxTokens in SDK catalog shapes (#12743)
* fix(models): tolerate null contextWindow/maxTokens in SDK catalog shapes

Live LiteLLM proxies report unknown model limits as explicit nulls in
/model/info (e.g. max_tokens: null). adaptSdkModelInfo only tolerated
undefined, so a single such model failed the entire catalog refresh and
left the model picker empty. Treat null like a missing value (matching
the existing pricing handling) and fall back to the safe defaults.

* Update apps/vscode/src/sdk/model-catalog/shape-adapter.ts

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

* Update apps/vscode/src/sdk/model-catalog/shape-adapter.ts

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

* fix(models): restore missing limit fallbacks

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
2026-07-30 20:19:56 +02:00
Sufiyan Khan fba27b3c81 fix(vscode): preserve draft when toggling from act to plan (#12241)
* fix(vscode): preserve draft when toggling from act to plan

* fix(vscode): simplify mode toggle draft handling
2026-07-30 11:17:57 -07:00
Saoud Rizwan 6549bdbacc Show edited file after the diff preview closes (legacy parity) (#12731)
* Show edited file after diff preview closes, matching legacy behavior

* Add changeset

* Reveal destination after apply patch moves

* Skip reveal for superseded previews and aborted edits
2026-07-30 11:17:43 -07:00
Bee 55ccbb7d51 fix(cli): remove rendering multiple views in one Bun process (#10936)
* fix(cli): open history in the existing TUI

* refactor(cli): clarify history TUI startup target

* feat(cli): add history actions to TUI

* fix(cli): avoid empty session when resuming history

* fix(cli): fail history delete without session id

* fix(cli): dispatch resume hook from history picker
2026-07-30 11:16:51 -07:00
Tran Binh Minh f039b1419f fix(vscode): show per-file diff for multi-file apply_patch (#12086)
* fix(vscode): show per-file diff for multi-file apply_patch

apply_patch edits to multiple files rendered the entire multi-file patch in every per-file diff row. Split the patch into one tool message per file at content_end (mirroring the read_files split) so each row shows only that file's changes.

Closes #9904

* fix(vscode): address review on multi-file apply_patch split

Import the canonical PATCH_MARKERS from @cline/core instead of the local AP_MARKERS duplicate and export it through the core barrel. The cross-world import barrier the old comment claimed does not exist - apps/vscode already imports runtime values from @cline/core.

Route the apply_patch branch in sdkToolToClineSayTool through getApplyPatchString so the streaming and finalized rows derive their content from one source.

Handle the bare-string apply_patch input. ApplyPatchInputUnionSchema accepts { input: string } | string; a bare two-file patch made getApplyPatchString return undefined, so both content_start and content_end produced one empty-path row instead of the per-file split. Return the raw string when the field lookup finds nothing, with a start/end reconciliation test.

Refs #9904

---------

Co-authored-by: Minhkunn <minh.12072k6@gmail.com>
2026-07-30 11:13:45 -07:00
Saoud Rizwan 97c558ffe8 chore(ui): release v0.2.0-next.1 2026-07-30 11:02:02 -07:00
Saoud Rizwan 8f6f1652e1 Improve slash command description contrast on selection (#12742)
* fix slash command hover text contrast

* remove slash menu regression test
2026-07-30 10:40:54 -07:00
cline-cloud[bot] 26ee3abf82 fix(core): stabilize Windows SDK tests (#12722)
* fix(core): stabilize Windows SDK tests

* fix(core): handle late subprocess stdin errors

* test(core): verify shell process cleanup

* test(core): budget Windows PowerShell hook

---------

Co-authored-by: Cline Bot <bot@cline.bot>
Co-authored-by: Dominic Cooney <dominic.cooney@cline.bot>
2026-07-30 19:01:15 +02:00
cline-cloud[bot] d2b674bb9d fix(vscode): restore macOS E2E launch (#12726)
Co-authored-by: Dominic Cooney <dominic.cooney@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-30 03:04:21 -07:00
Saoud Rizwan 2ce4facd9d fix(vscode): show thinking immediately after submit (#12733)
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-30 02:59:38 -07:00
Saoud Rizwan ac4724166d Disable feature tips by default in the VS Code extension (#12730)
* Disable feature tips by default in VS Code extension

* Add changeset for feature tips default change
2026-07-30 00:58:42 -07:00
Cline Test 2131bbba0c fix(connectors): recover Slack thread mapping when session is gone (#12727)
* fix(connectors): recover Slack thread mapping when session is gone

A connector thread binding can outlive its runtime session (hub restart,
session abort, retention cleanup). When that happened the thread stayed
pinned to a dead session id and every subsequent turn failed with
`session_not_found`, so the bot replied "Slack bridge error: session not
found" forever with no way to recover short of editing threads.json.

Drop the stale binding and replay the turn once against a brand new
session. Both the normal turn path and the steering path are covered.

Adds forgetThreadSession() to session-runtime and 3 regression tests.

* fix(connectors): serialize stale session recovery

---------

Co-authored-by: cline-test-bot <cline-test-bot@users.noreply.github.com>
Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-29 23:57:53 -07:00
Saoud Rizwan 792738eeed fix(vscode): auto-proceed long-running terminal commands (#12712)
* fix(vscode): auto-proceed long-running terminal commands

* chore(vscode): raise terminal auto-proceed timeout to 60s

* chore(vscode): raise terminal auto-proceed timeout to 300s
2026-07-29 23:30:52 -07:00
Saoud Rizwan 0ac7d0bdb5 remove Enable R1 messages format option from OpenAI Compatible provider (#12729) 2026-07-29 23:28:02 -07:00
Saoud Rizwan 4fa42771a7 Consolidate per-provider model refresh handlers onto the SDK catalog (#12716)
* Consolidate per-provider model refresh handlers into the SDK

The VS Code extension resolved model catalogs from two sources: the SDK
catalog (models.dev-backed) used by resolveModelInfo/task header, and
host-side refresh handlers (refreshOpenRouterModels & co.) used by the
settings pickers. This dual-source split produced inconsistencies like
ENG-2345.

SDK (@cline/core):
- New rich live model sources (live-model-sources.ts) ported from the
  extension handlers: OpenRouter (pricing incl. cache read/write,
  descriptions, image support, thinking config, tiers/global-endpoint
  metadata, curated overrides, stealth models), Vercel AI Gateway, and
  Hugging Face. Keyed by generated catalog key so cline shares
  OpenRouter's live data.
- mergeKnownModels layers rich live entries field-wise on top of the
  curated catalog (live fields win, curated fields fill gaps) instead of
  the modelsSourceUrl replace semantics.
- New Groq and Requesty private fetchers (API-key gated); Baseten
  private fetcher now parses live pricing and reasoning support and is
  enriched from the curated catalog.

Extension (apps/vscode):
- refreshOpenRouterModels/Groq/Baseten/VercelAiGateway/HuggingFace/
  Hicap/Requesty are now thin delegates over the SDK provider catalog;
  all bespoke fetch/parse/disk-cache code is deleted.
- shape-adapter maps the SDK's thinkingConfig, temperature,
  global-endpoint capability, and metadata tiers onto the extension
  ModelInfo.
- Removed the now-unused StateManager models cache, per-provider disk
  cache files, and the dead readOpenRouterModels stub.

Fixes ENG-2381.

* Simplify: rely on the SDK's models.dev catalog, no rich live sources

Drop the ported per-provider live fetchers and curated overrides
(live-model-sources.ts) and all SDK merge changes. The extension now does
exactly what the CLI does: refresh handlers resolve through
resolveProviderConfig, which serves the models.dev-backed catalog
(bundled + runtime live refresh) plus the SDK's pre-existing
authenticated fetchers (Baseten/Hicap/LiteLLM/Poolside). No hardcoded
model info or per-model pricing workarounds remain anywhere.

Also reverts the shape-adapter additions since no SDK catalog source
populates thinkingConfig/temperature/metadata tiers today.

* Replace thinking-budget sliders with catalog-driven reasoning effort selection

Match the CLI's UX: every reasoning-capable model (SDK catalog
'reasoning' capability -> supportsReasoning) gets the Reasoning Effort
selector (none/low/medium/high/xhigh); the legacy 'Enable thinking' +
budget-tokens slider is removed everywhere, along with the hardcoded
per-provider thinking-model id lists (Anthropic, Claude Code, Bedrock,
Qwen) and claude/grok model-id heuristics in the OpenRouter, Vercel,
and Requesty pickers.

Effort changes now dual-write the provider-config reasoning settings
({enabled, effort}) that the session factory actually consumes - the
budget slider wrote legacy plan/act thinkingBudgetTokens state that
sessions already ignored. The utility request path
(buildSdkProviderConfig) drops its budget preference and forwards
effort only; the SDK translates effort into each provider's wire
format (including budget-token mapping where required).

* Gate picker reasoning-effort UI on live catalog entries

The OpenRouter/Vercel/Requesty pickers read the committed legacy
model-info snapshot, which provider-config writes can clear when a
resolution lands on a fallback source - selecting an effort made the
selector disappear. Gate on the live catalog map (with snapshot
fallback) instead; Requesty gates on the catalog only, since its
safe-default fallback over-reports reasoning support.

* Address review: honor legacy thinking budgets, dedupe refresh handlers

- Persisted thinking budgets are honored again (greptile P1 / review
  request): normalizeProviderReasoningSettings maps a stored
  reasoning.budgetTokens (written by older versions or the SDK's
  legacy-state migration) onto the effort scale and treats it as
  thinking-on, and buildSdkProviderConfig derives an effort from the
  legacy plan/act budget fields when no explicit effort exists. An
  explicit 'none' still wins. Shared mapping lives in
  reasoningEffortFromThinkingBudget with low/medium/high buckets.
- Extract resolveProviderModelsRecord into providerCatalogShared and
  collapse the seven refresh handlers onto it.
- Document the explicit OCA decision: its reasoning control is the
  API-driven effort dropdown; the removed budget slider wrote state no
  OCA request path consumed.

* Harden OpenRouter picker reasoning gate against placeholder metadata

Gate on the raw committed model-info snapshot instead of the hook's
default-info fallback, so a selected id that is absent from the catalog
can never inherit reasoning support from placeholder metadata (the
fallback carries no supportsReasoning today, but reading the raw field
removes the latent dependency).
2026-07-29 23:27:05 -07:00
Saoud Rizwan c91cd4be59 fix(vscode): clear pending approvals on task switch (#12705)
* fix(vscode): clear pending approvals on task switch

* feat(vscode): show compact slash command

* docs(vscode): explain task approval cleanup

* fix(vscode): settle pending questions on cleanup

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-29 22:15:12 -07:00
Saoud Rizwan 851ee033bc Fix checkpoint restores across session resumes (#12713)
* fix checkpoints across session resumes

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style checkpoints mapping helper

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-29 22:08:37 -07:00
Saoud Rizwan 192e6ffab2 fix(vscode): interrupt running terminal command when a task is cancelled (#12696)
Cancelling a task previously only detached Cline's listeners from an
in-flight foreground command (process.continue()); the spawned process
kept running in the user's terminal after cancellation.

Send Ctrl+C (ETX) to the terminal before detaching so the shell delivers
SIGINT to the foreground process group, actually stopping the command.
The terminal is left open for reuse, and cancellation still succeeds even
if the interrupt write throws (e.g. terminal already disposed).
2026-07-29 21:45:43 -07:00
Saoud Rizwan 6b668fefdc fix(core): carry legacy OpenAI-compatible model-info overrides into seeded models.json (#12697)
The legacy-provider migration seeded the openai-compatible models.json
entry with hardcoded defaults (contextWindow 128k, no pricing/temperature/
maxTokens/R1 flag). Because later override migrations skip models that
already exist in models.json, the user's legacy planMode/actModeOpenAiModelInfo
overrides were silently discarded on first upgrade: context window,
max output tokens, input/output prices, temperature, supportsImages=false,
and isR1FormatRequired all reset to defaults.

Seed the entry from the mode-appropriate legacy model-info snapshot
instead, treating legacy sentinels (maxTokens -1, temperature 0,
prices 0) as unset.
2026-07-29 21:31:55 -07:00
Saoud Rizwan 8224ad1634 feat(llms): default OpenRouter provider to anthropic/claude-sonnet-5 (#12707)
Matches the legacy extension's OpenRouter default (openRouterDefaultModelId),
so users migrating from the legacy build without an explicitly selected model
keep the same default model instead of being silently moved to
anthropic/claude-sonnet-4.6.
2026-07-29 17:50:57 -07:00
Saoud Rizwan 96e8f51436 fix(core): don't abort config scan when .clinerules is a legacy single file (#12702)
A legacy single-file .clinerules at the workspace root made the config
watcher's scans of .clinerules/skills and .clinerules/workflows throw
ENOTDIR, which aborted the entire user-instruction refresh: workspace
rules, global rules, and the Skills view all silently failed to load.

Treat ENOTDIR like ENOENT in isIgnorableDirectoryError so a file in a
directory position simply yields no candidates. The .clinerules file
itself is still picked up by the file branch of discoverRulesLikeFiles.
2026-07-29 17:48:23 -07:00
Saoud Rizwan 5e5c4475bc fix(vscode): surface VS Code LM as a host provider (#12711)
* fix(vscode): register VS Code LM provider in catalog

* fix(vscode): use empty selector as vscode-lm catalog default model
2026-07-29 17:43:10 -07:00
Saoud Rizwan 66228fb30b Hide Plugins tab in extension Customize view (#12720) 2026-07-29 17:40:11 -07:00
Saoud Rizwan 7b8798c996 Fix built-in slash commands on the SDK runtime: /newtask aliases /compact, port /deep-planning expansion, hide /newrule and /reportbug (#12721)
* fix(vscode): hide /newrule and /deep-planning until their prompt expansions are ported to the SDK runtime

* feat(vscode): port the /newtask context handoff to the SDK runtime

Expand /newtask into explicit new_task-tool instructions in
SdkController.resolveSlashCommands (ported from legacy
newTaskToolResponse), register a custom new_task AgentTool that captures
the model-generated context summary and completes the run, and emit the
ask:"new_task" message on turn completion so the existing webview
"Start New Task with Context" button (which preloads a fresh task with
the ask text) becomes reachable again. Set the turn phase to
awaiting_followup when emitting the ask, since the completesRun
termination path skips the translator's usual end-of-turn status
handling.

* fix(vscode): hide /reportbug until its prompt expansion is ported to the SDK runtime

Also drop the feature tip promoting /reportbug so the UI doesn't
advertise a command that no longer autocompletes.

* Revert "feat(vscode): port the /newtask context handoff to the SDK runtime"

This reverts commit d9ad153aec.

* feat(vscode): make /newtask an alias of /compact

Condensing achieves /newtask's goal (continue working with a fresh,
summarized context window) without the legacy new_task tool, so the
webview intercepts /newtask alongside /compact and /smol and runs the
condense RPC. Menu description updated to match.

* feat(vscode): port the /deep-planning prompt expansion to the SDK runtime

Expand /deep-planning into the legacy generic-variant instructions
(silent investigation, targeted questions, implementation_plan.md) in
SdkController.resolveSlashCommands, ahead of workflow/skill expansion.
Legacy's STEP 4 created an implementation task via the new_task tool,
which doesn't exist on the SDK runtime; the ported prompt instead has
the agent present the plan and wait for explicit user confirmation.
Re-adds /deep-planning to the slash menu.

* refactor(vscode): simplify the /deep-planning expansion

Drop the custom regex/expander and shell-specific research-command
blocks: the builtin is now a plain AvailableRuntimeCommand appended to
the discovered workflow/skill commands, so the existing
expandSlashCommands machinery handles matching and replacement. The
prompt keeps the four-step protocol and implementation_plan.md
structure with a generic investigation paragraph instead of embedded
OS-specific commands.
2026-07-29 17:36:33 -07:00
John Choi dfd22bf79e refactor(ui): extract desktop approval card (#12693)
* refactor(ui): extract desktop approval card

* fix(ui): preserve approval card parity

* refactor(ui): keep approval labels fixed
2026-07-29 17:22:22 -07:00
John Choi 307707fd00 refactor(ui): extract desktop search combobox (#12663)
* refactor(ui): extract desktop search combobox

* fix(ui): preserve search selector visual parity

* fix(ui): preserve search combobox parity

* fix(ui): preserve combobox adoption parity

* test(desktop): reflect combobox accessible names

* fix(ui): disable open combobox options
2026-07-29 16:25:50 -07:00
Dominic Cooney 2a47c6ca08 fix(mcp): honor per-server timeout (seconds) across all clients (#12546)
* fix(mcp): honor per-server timeout (seconds) across all clients

The per-server timeout field in cline_mcp_settings.json was only read
by the VSCode extension's tools/call path. Everywhere else used
hardcoded constants: the SDK client timed out all requests at 5s and
initialize at 1.5s, and the extension's metadata requests (tools/list,
resources/*, prompts/*) timed out at 5s. Slow servers failed despite a
configured timeout (#7635, #12344).

Resolve the timeout once per client and apply it to every request:

- @cline/shared exports the default (60s) and bounds (1s-3600s) plus a
  resolver that clamps out-of-range values, so a milliseconds/seconds
  mix-up can no longer become hours.
- The SDK config loader parses timeout into
  McpServerRegistration.timeoutSeconds; StdioMcpClient and
  SdkUrlMcpClient apply it to initialize, tools/list, and tools/call.
  Unconfigured servers keep the fast 1.5s initialize probe so startup
  is no slower than before; a configured timeout raises that budget
  for slow-starting servers.
- The extension routes every request (including metadata) through one
  resolver and drops the hardcoded 5s DEFAULT_REQUEST_TIMEOUT_MS.
- createMcpTools derives the agent tool timeoutMs from the same value,
  keeping the wrapper and request timeouts in agreement.
- Timeout errors now name the bound and the field to increase; the
  VSCode server row and the CLI server list show the effective timeout
  and how to change it.

* fix(mcp): harden timeout lifecycle handling

* fix(mcp): address timeout review feedback

* fix(mcp): bound initialization and reconnect

* fix(mcp): keep timeout snapshots consistent

* fix(mcp): use standard stdio framing

* fix(mcp): bound legacy stdio fallback

* fix(mcp): honor timeout in framed fallback

* test(vscode): use SDK Vitest runner

* fix(mcp): fetch server capabilities in parallel

The four post-connect metadata requests (tools/list, resources/list,
resources/templates/list, prompts/list) ran sequentially, so a server
that hangs after initialize blocked connectToServer for four timeout
bounds. The MCP client correlates concurrent requests by JSON-RPC id
and the stdio transport writes each message atomically, so the fetches
now run in parallel and the worst case is one bound.

Also delete McpHub.readResource and McpHub.getPrompt and their response
types: nothing calls them since the SDK migration removed the
access_mcp_resource tool and prompt expansion.

* fix(mcp): keep failed servers and both framing errors visible

When both stdio framing attempts fail differently during initialize,
name each attempt's error instead of discarding the Content-Length
fallback's diagnostics. When they fail identically (both timed out),
rethrow the newline error unchanged so the timeout hint is the whole
message.

When connectToServer fails before the connection is registered (e.g.
the transport fails to start), register a disconnected entry carrying
the error so the server stays visible in the list instead of silently
disappearing, and notify the webview so the row leaves the connecting
state.

* fix(mcp): reject tool calls on connections without a client

A failed (re)connect registers a disconnected entry with a null client
so the server stays visible in the list. A tool wrapper captured by an
active session can still target that server; callTool now rejects it
with a controlled error naming the server and its last connection
error, instead of dereferencing the null client and throwing a
TypeError.
2026-07-29 16:25:23 -07:00
Edoardo Busano 704e953b69 fix(shared): keep valid OTEL headers when one entry is malformed (#12260)
parseKeyPairsIntoRecord wrapped the whole forEach in one try/catch, so a single entry that broke decodeURIComponent (e.g. a stray % in OTEL_EXPORTER_OTLP_HEADERS) aborted the loop and silently dropped every remaining header. Move the try/catch inside the loop to skip only the malformed entry. Adds regression tests.
2026-07-30 00:47:44 +02:00
John Choi c5661f8835 refactor(ui): extract desktop quick actions (#12664)
* refactor(ui): extract desktop quick actions

* fix(ui): preserve quick action visual parity

* style(ui): format quick actions import

* style(ui): format packaged component files
2026-07-29 14:52:05 -07:00
Ara 60c5a24eef Route reasoning controls from models.dev across providers (#12542)
* refactor(reasoning): route model controls from models.dev

Preserve typed reasoning capabilities from the model catalog, normalize requests once against advertised effort, budget, and toggle controls, and keep provider adapters focused on wire encoding. CLI presentation changes are intentionally deferred to a follow-up.

* fix(llms): encode catalog reasoning controls per provider

* fix(llms): clamp reasoning defaults and budgets

* refactor(shared): narrow reasoning exports

* refactor(llms): colocate reasoning controls

* fix(llms): handle mandatory Claude reasoning modes

* fix(llms): omit impossible Anthropic thinking

* fix(llms): reject impossible Anthropic thinking
2026-07-29 23:43:59 +02:00
John Choi 3f9ed573db refactor(ui): extract desktop aurora (#12665)
* refactor(ui): extract desktop aurora

* docs(ui): preserve aurora constraints

* docs(ui): document aurora container contract

* style(ui): format package file list
2026-07-29 14:30:56 -07:00
Bee ac432c87f0 fix(desktop): prevent long-running chat turns from timing out (#12671)
* fix(desktop): disable timeout for chat send commands

Add per-invocation timeout options to the desktop client and disable the deadline for long-running chat send requests. Extract the shared command response type and verify send commands use the timeout override.

* fix(desktop): clean up failed websocket sends
2026-07-29 23:30:28 +02:00
Etisha Garg 95b841a2dd docs: add screenshots for finding free models (#12690) 2026-07-29 12:50:43 -07:00
Saoud Rizwan 7d63376d98 Revert "Revert "docs: add Cline free models page (#12183)" (#12185)" (#12186)
This reverts commit ed3107f9ec.

Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
2026-07-29 07:41:43 -07:00
Sufiyan Khan 912c467818 fix(vscode): cancel Cline task on signout (#12657) 2026-07-29 00:41:33 -07:00
Saoud Rizwan c39c6d4479 feat(vscode): enable Cline Pass unconditionally, removing the ext-cline-pass feature flag (#12677) 2026-07-29 00:23:32 -07:00
Saoud Rizwan 071e5451b1 fix(webview): use theme-colored dropdowns for native select elements (#12676) 2026-07-28 23:55:28 -07:00
Saoud Rizwan 159961b3a3 Fix useDebouncedInput firing onChange on mount, which could wipe stored API keys (#12675)
useDebouncedInput scheduled its debounced onChange on mount and on every
external initialValue resync, not just user edits. Settings fields mount
with a placeholder value while their backing provider config is still
loading asynchronously, so the mount-fire echoed that placeholder back
to the backend ~100ms later.

For DebouncedTextField-backed secret fields (e.g. the OpenRouter API key,
which renders a masked value derived from the async readProviderConfig
response), losing that race meant writing apiKey: "" — silently deleting
the stored key from both providers.json and the legacy secrets store,
and leaving the field rendering empty despite a previously persisted key.
Non-secret fields similarly re-saved stale placeholder values on every
mount.

Gate the debounced save on an actual user edit: only values set through
the returned setter fire onChange; mount and external resyncs never do.
2026-07-28 22:39:13 -07:00
Saoud Rizwan 4a3f1ce310 fix(openai-compatible): carry custom model metadata across model-id changes (ENG-2341) (#12628)
* fix(openai-compatible): keep user model metadata when only the model id changes

Changing the OpenAI Compatible model id committed the new id without
overrides, so an id unknown to the catalog resolved to safe defaults
(inputPrice/outputPrice 0, supportsPromptCache false) and paid requests
billed as $0.0000. The legacy extension kept this user-authored metadata
in a single id-independent blob, so custom prices survived id edits.

Recommit the currently displayed overrides under the new id when the
model id changes, and let edits made while that commit is round-tripping
target the pending id instead of the stale read-back id.

Fixes ENG-2341

* fix(openai-compatible): scope pending selection state per mode

Review follow-up: the pending-override accumulator and pending-commit
counter were shared across Plan and Act. Changing the Act model id,
switching to Plan while that commit was round-tripping, then editing an
override committed the Plan edit under the pending Act model id (and the
shared pending count blocked Plan's reseed at the mode boundary).

Record the mode alongside the pending selection and only trust it for
edits in the same mode, keep per-mode pending counts so a mode switch
reseeds from that mode's committed state, and cover the deferred-commit
mode-switch scenario with a component test.

* fix(openai-compatible): give each mode its own pending-selection accumulator

Review follow-up: tagging the single shared accumulator with a mode still
lost state on a mode round trip. With an Act commit pending, visiting
Plan reseeded the shared slot to Plan; returning to Act could not reseed
(Act's read-back was still in flight), so the next Act edit merged onto
an empty set and silently dropped the pending prices/context/capabilities.

Keep one accumulator slot per mode so a round trip through the other
mode never disturbs a mode's pending state, and cover the scenario with
a deferred-commit round-trip test.
2026-07-28 22:30:19 -07:00
Saoud Rizwan 10a658a767 fix: report OpenRouter Anthropic models' full 1m context window consistently (#12629)
The OpenRouter model picker (refreshOpenRouterModels) still applied the
legacy 200k context-window restriction to Anthropic Claude models, while
the task header and auto-compaction resolve model info through the SDK
catalog, which reports the full 1m extended context window. The same
model showed Context: 200K in the picker and 1.0m in the task header.

Per the current product direction the 200k restriction (and its :1m
opt-in variants) is dropped entirely — everyone gets the 1m context
window. Remove the artificial clamps from refreshOpenRouterModels
(keeping the prompt-cache pricing overrides) and update the
openRouterDefaultModelInfo fallback to match, so the picker, the task
header, and compaction thresholds all agree on 1m.

Closes ENG-2345.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 22:04:55 -07:00
Dominic Cooney 9d63bfcb31 fix(vscode): restore foreground terminal default (#12672)
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-28 22:01:31 -07:00
Saoud Rizwan daa32ee138 fix(core): migrate legacy API keys for all secret-backed providers (ENG-2337) (#12626)
* fix(core): migrate legacy API keys for all secret-backed providers

collectCandidateProviderIds only nominated 11 provider ids while
buildLegacyProviderSettings can copy keys for 34, so stored keys for the
other 25 providers (deepseek, mistral, xai, groq, ...) were silently
dropped during migration unless the provider was the active plan/act
provider. Add the missing candidate checks so any stored key makes its
provider a migration candidate.

Also pick the legacy mode per candidate: a split plan/act config applied
the single globalState.mode to every provider, so the non-current mode's
configured model was replaced by the catalog default.

Migration re-runs on manager construction and never overwrites existing
entries, so users who already ran the buggy migration get dropped keys
backfilled from the still-present legacy secrets.json on next launch.

Fixes ENG-2337

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): normalize legacy provider-id aliases during migration

Address review: the mode selection and model fallback compared raw
legacy provider ids, so a declared alias (togetherai -> together,
sap-ai-core -> sapaicore) in globalState would miss its canonical
secret-derived candidate, read the wrong mode, and could write duplicate
alias/canonical entries. Route candidate collection, mode comparison,
and the generic model fallback through the existing normalizeProviderId
boundary. resolveMigratedProviderId now delegates to normalizeProviderId
(identical for the openai -> openai-compatible case it already handled).

Legacy ApiProvider never actually stored alias forms, so this is
hardening for hand-edited state rather than a live regression.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 21:59:39 -07:00
Saoud Rizwan a51fa646bb fix(vscode): reconcile the two provider state stores (ENG-2332) (#12640)
* fix(vscode): reconcile the two provider state stores (ENG-2332)

- createStorageContext now honors CLINE_DATA_DIR with the same priority as
  the SDK's resolveClineDataDir and the legacy reader's resolveDataDir
  (explicit option > CLINE_DATA_DIR > CLINE_DIR/data > ~/.cline/data), so
  globalState.json/secrets.json live in the same data dir as providers.json
  and legacy task state instead of silently splitting across directories.
- Add setLastUsedProvider and call it on active provider switches
  (SdkProviderChangeCoordinator) and when a session resolves its provider
  from StateManager (buildSessionConfig), so providers.json's
  lastUsedProvider no longer goes stale across provider switches.
- Trim env vars in legacy-state-reader's resolveDataDir to match the SDK's
  resolution exactly.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: trim ENG-2332 fix to the minimal change set

Revert the cosmetic legacy-state-reader trim, restore the original CLINE_DIR
line in createStorageContext, and tighten comments. No behavior change to
the two core fixes.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop lastUsedProvider sync, keep only the data-dir alignment fix

Scope ENG-2332 to the root-cause fix: createStorageContext honoring
CLINE_DATA_DIR like the SDK resolvers. The providers.json lastUsedProvider
staleness is deferred to a follow-up.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: trim CLINE_DATA_DIR in resolveDataDir to match createStorageContext

Addresses Greptile P1: a whitespace-padded CLINE_DATA_DIR was trimmed by
createStorageContext but used verbatim by the legacy reader, which could
resolve the two stores to different directories again.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: retrigger CI (windows e2e flake in chat.test.ts)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: share one data-dir resolver between storage context and legacy reader

Per review feedback: extract resolveDataDirFromEnv in storage-context.ts and
have legacy-state-reader's resolveDataDir delegate to it, so the two stores
structurally cannot drift apart again.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: trim CLINE_DIR in the shared data-dir resolver to match the SDK

The SDK's resolveClineDir trims CLINE_DIR; a whitespace-padded value would
otherwise still resolve VS Code state and providers.json to different
directories.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 21:36:02 -07:00
John Choi 5213070f67 refactor(ui): extract desktop hero heading (#12609)
* refactor(ui): extract desktop hero heading

* test(ui): preserve hero heading constraints

* docs(ui): trim hero heading comments
2026-07-28 19:48:32 -07:00
Saoud Rizwan 7d7cddb9d9 chore(desktop): release v0.0.7 2026-07-28 18:33:51 -07:00
Saoud Rizwan 55e169f1f8 feat(vscode): working-directory badge in task header for out-of-workspace tasks (#12637)
* feat(vscode): show working-directory badge when a task runs outside the open workspace

Tasks resumed from the CLI or another workspace keep their original cwd,
so Cline reads, edits, and runs commands in a directory that is not the
one visible in the window - previously with no indication anywhere.

- Add TaskWorkingDirectoryBadge: a persistent warning chip in the task
  header (folder icon + cwd basename, full path + explanation in the
  tooltip) shown only when the task cwd is neither an open workspace
  root nor inside one. Hidden when roots or cwd are unknown to avoid
  false positives.
- Fix SdkController.getStateToPostToWebview to pass its workspace
  manager into the shared state builder; the SDK path previously always
  sent workspaceRoots: [] to the webview.
- Unit tests for the outside-workspace predicate (case, separators,
  multi-root, prefix collisions) and badge render states.

* fix(vscode): platform-aware path comparison in working-directory badge

Address PR #12637 review findings:
- Case folding is now platform-aware (win32/darwin insensitive, linux
  and unknown strict), so case-only path differences on Linux are no
  longer hidden; mirrors arePathsEqual in src/utils/path.ts.
- Backslashes are treated as separators only on win32; on POSIX a
  backslash is an ordinary filename character.
- Containment prefix no longer doubles the separator when a workspace
  root already ends with one, fixing false warnings for '/' and drive
  roots.
- Tests cover case-only pairs under win32/darwin/linux/unknown,
  POSIX-backslash filenames, '/' and 'C:\' workspace roots.

* fix(vscode): make darwin path comparison strict in working-directory badge

Follow-up to PR #12637 review: darwin volumes can be case-sensitive, and
the host's canonical arePathsEqual (src/utils/path.ts) already treats
only win32 as case-insensitive. Align the badge predicate with that
convention: case folding and backslash separators apply on win32 only;
darwin, linux, and unknown compare strictly. For a warning badge a rare
spurious warning beats silently hiding a real mismatch.
2026-07-28 18:23:41 -07:00
Saoud Rizwan c227e1ae36 fix(vscode): restore legacy workflow invocation and management UI (#12562)
* fix(vscode): restore legacy workflow invocation and management UI

- Expand /workflow slash commands typed with the legacy .md filename
  spelling (what the autocomplete menu inserts) and mid-message, and
  honor the user's workflow enable/disable toggles, instead of only
  expanding a leading extension-less /name via the SDK resolver.
- Restore the Workflows tab in the rules modal (view, toggle, create,
  edit, delete; enterprise section) that was dropped in the SDK-backed
  extension while all its gRPC handlers remained wired.

* chore: add changeset for workflow fixes

* fix(vscode): refresh workflow toggles on webview launch

The slash command menu is driven by workflowToggles state, but nothing
refreshed it at startup in the SDK-backed extension (only opening the
rules modal or creating a rule file did), so workflows never appeared in
the chat autocomplete until the user opened the modal. Legacy refreshed
toggles on task init.

* feat(vscode): move Workflows tab last and add deprecation warning

Workflows tab now appears after Rules/Hooks/Skills, and its view leads
with a warning banner: workflows are being deprecated in favor of
skills, with a docs link.

* chore: update changeset for workflow deprecation notice

* fix(vscode): address review findings on workflow expansion

- Honor remoteWorkflowToggles (and locked alwaysEnabled remote
  workflows) when building the disabled set, so disabled enterprise
  workflows no longer expand.
- Treat a workflow as disabled only when no scope has it enabled, so a
  disabled workspace file no longer shadows a same-named enabled global
  one (legacy expanded the enabled scope).
- Strip all workflow extensions the SDK discovers (.md/.markdown/.txt)
  when matching typed commands, not just .md.
- Re-read toggle state after the async directory scan in
  refreshWorkflowToggles so a toggle flipped mid-scan is not overwritten
  by the stale snapshot.

* fix(vscode): map workflow toggles to records so frontmatter names are governed

Compute the disabled set from the discovered workflow records
(listRecords) instead of toggle-path basenames alone: a file's toggle is
matched by its basename and disables the record's actual command name,
so a frontmatter 'name' that differs from the filename is still governed
by the Workflows toggle. Remote-config-materialized records are governed
by the name-keyed remote toggles (locked alwaysEnabled remain on).

* fix(vscode): harden workflow toggle-name mapping for expansion

- A command name shared by several records now counts as enabled when
  any record is enabled, so a disabled local workflow can no longer
  suppress an enabled or locked (alwaysEnabled) enterprise workflow.
- Remote toggles/locks are matched via a sanitizeSegment-compatible key,
  so config names that get rewritten during materialization (e.g. 'Org
  Standards' -> org-standards.md) still govern expansion.
- Typed filenames (e.g. /my-workflow.md from autocomplete) now resolve
  to workflows whose frontmatter renames the command, via the record's
  file basename.

* fix(vscode): govern each workflow command by its own record's toggle

Key the disabled set by exact command name and decide each record
independently instead of OR-aggregating by canonical name: distinct
commands whose names only differ by case or extension (e.g. a local
'Release' and a remote 'release') no longer influence each other, so an
enabled local workflow cannot keep a disabled enterprise workflow
expandable, and a disabled one cannot suppress a locked enterprise
workflow.

* fix(vscode): exact remote-name sanitization and keep mid-scan toggle additions

- Port @cline/shared's sanitizeSegment verbatim (incl. the 80-char cap)
  for remote workflow name comparison, so long enterprise workflow names
  cannot bypass a disabled toggle after filename truncation.
- The post-scan toggle merge now also keeps entries added while the scan
  was running (e.g. a workflow created via the modal), instead of
  pruning them with the deleted files.

* fix(vscode): handle mid-scan deletions and sanitized remote-name collisions

- The post-scan toggle merge now also drops entries that were removed
  from state while the scan ran, so a workflow deleted mid-refresh is
  not restored by the stale scan result.
- Remote toggle names that sanitize to the same materialized name merge
  as enabled-if-any-enabled instead of last-write-wins.

* fix(vscode): serialize workflow toggle refreshes

Queue refreshWorkflowToggles runs on a promise chain so overlapping
refreshes (webview launch, modal open, file create/delete) cannot
interleave scans and writes. Combined with the post-scan merge for
direct toggle flips, this closes the remaining stale-refresh races.

* fix(vscode): key remote workflow toggles off the materialized filename

The materializer names remote workflow files from the config name, so
derive the remote toggle key from the file basename instead of the
parsed command name; a frontmatter alias can no longer bypass a
disabled remote toggle.
2026-07-28 18:20:54 -07:00
Saoud Rizwan d0a0c802af fix(vscode): interrupted tasks disappear from History (ENG-2336) (#12613)
* fix(vscode): make interrupted tasks findable in History and restore Resume button

Interrupted/cancelled sessions were presented as gone (ENG-2336):

- History fuzzy search used location-based Fuse scoring (ignoreLocation:
  false, threshold 0.6), so any match more than ~60 characters into the
  task title scored above the threshold and the task silently vanished
  from search results even though it was in the list. Search now matches
  anywhere in the title.

- Opening a task from History never updated the authoritative TurnState,
  so the footer kept the previous context's phase (usually idle) and the
  Resume Task button never appeared for interrupted/failed sessions.
  showTaskWithId now derives the phase from the reopened conversation:
  resumable for interrupted tasks, completed for completed ones.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): decide Resume vs Start New Task from persisted session status

SDK conversations do not record a completion tool call in the transcript
(a completed turn and one interrupted mid-stream both end with plain
assistant text), and history rendering appends a synthetic trailing
ask:"completion_result" either way, so the message tail always looked
"completed". Reopening a task from History now reads the persisted
session status: "completed" gets the Start New Task affordance, while
cancelled/failed (interrupted) sessions get Resume Task. When reopening
the currently-active task, the stop is awaited first so the status read
reflects how the last turn actually ended.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): fence concurrent history opens and default unknown status to Resume

Address review feedback:

- showTaskWithId now takes a generation fence: a request that loses the
  race to a newer showTaskWithId or clearTask abandons installation after
  its awaited reads, so a slow older request can never clobber the user's
  latest selection (task proxy, messages, or turn phase). clearTask bumps
  the generation too so New Task wins over an in-flight history open.

- The resume affordance no longer falls back to the message tail when the
  persisted session status is unavailable: the tail always ends with the
  synthetic ask:"completion_result" that history rendering appends, which
  misclassified interrupted tasks as completed on a failed status read.
  Only an explicit "completed" status gets Start New Task; anything else
  (including unknown) gets Resume Task, the safe direction.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): allocate history-open generation before the lookup and fence before session stop

Address review feedback: the latest-selection-wins fence started too late.
SdkController.showTaskWithId awaited findHistoryItem() before entering the
coordinator, so a stalled preflight for an older selection could re-enter
with a NEWER generation than a later selection and replace it — and since
the first fence check sat after endActiveSession, a superseded request
could also stop a session the newer selection had just installed.

The history lookup now lives inside the coordinator (skipHistoryLookup is
gone), the generation is allocated synchronously before all asynchronous
work, and a fence check runs before endActiveSession so a superseded open
never stops the newer selection's session. The coordinator returns the
HistoryItem so SdkController keeps its TaskResponse contract. Regression
test covers the exact reported sequence: stalled lookup for task A, task B
selected and loaded, A resolves last — B stays installed and A stops
nothing.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 18:16:19 -07:00
Saoud Rizwan f9227fead4 Show completion feedback box for inferred turn-final responses in SDK path (#12638)
* Show completion feedback box for inferred turn-final responses in SDK path

The SDK agent usually ends a turn with a plain text response instead of an
attempt_completion / plan_mode_respond tool call, so the legacy green 'Task
Completed' box (act) and 'Plan Created' box (plan) never rendered in the new
extension — making a finished turn look stuck or frozen.

Now, when a turn ends cleanly (done reason 'completed', no completion tool
used) and its last content is a text response, that text row is retagged in
place to say:'completion_result' (act, green box) or the new
say:'plan_completion_result' (plan, yellow-accented 'Plan Created' box).

- Track the turn-final text candidate in MessageTranslatorState; cleared on
  tool activity, errors, aborts, and new user turns
- Replay the same inference during history rehydration, recovering each
  turn's plan/act mode from the persisted <user_input mode="..."> wrapper
- Add plan_completion_result ClineSay type (+ proto enum) rendered via
  PlanCompletionOutputRow, restyled with the plan-yellow accent to match
  the plan/act toggle and the CLI's plan color
- Turn phase semantics unchanged: footer buttons still come from TurnState

* Remove attempt_completion tool and strip completion box headers

- Drop the attempt_completion extra tool (and its shell-command executor)
  from VS Code SDK sessions; the SDK's built-in submit_and_exit is already
  disabled for act/plan presets, so the agent now always ends its turn with
  a plain text response and the turn-end inference styles it.
- Translator keeps recognizing attempt_completion/submit_and_exit for
  replaying persisted transcripts from older sessions.
- Remove the 'Task Completed' header, check icon, and copy button from the
  green completion box, and the 'Plan Created' header, notepad icon, and
  copy button from the yellow plan box. The final text of a turn may be a
  question rather than an actual completion or plan, so the boxes are now
  quiet color cues that make no claim.

* Skip completion retag for terminal text of failed/cancelled sessions

The trailing text of a session whose last run failed or was cancelled is a
dangling partial response, not a completion. Gate the history converter's
final synthesized turn end on the session record's status so reopening a
broken task keeps its terminal text as a plain row instead of an inferred
completion box. Mid-transcript turns are unaffected: the user continued
after them and history carries no per-turn outcome.

* Require clean at-rest session status before retagging terminal text

Tighten the negative failed/cancelled check into an allowlist: the history
converter now only retags the transcript's terminal text when the session
record is 'completed' (formally stopped clean run) or 'idle' (the normal
at-rest state between interactive turns). 'running'/'pending' at rest means
the process died mid-turn, so its dangling partial response stays plain.

* Restrict history completion retag to the transcript's final turn

Persisted SDK transcripts carry no per-turn outcome, so a mid-conversation
turn the user cancelled mid-response (then followed up on) is
indistinguishable from one that ended cleanly. Retagging those presented
interrupted responses as deliberate turn ends. History rehydration now only
retags the final turn's terminal text, gated on the session record's
at-rest status; earlier turns always render as plain text. Live sessions
are unaffected — their per-turn boxes come from real done events.

* Trust only status 'completed' for the history completion retag

'idle' is written by markTurnIdle for every interactive finish reason,
including aborted turns, so an at-rest idle record cannot prove the last
turn ended cleanly. Terminal statuses are reliably written when sessions
are released (task switch, clear, dispose), so requiring 'completed' keeps
the box on normal reopened tasks while never styling an interrupted
response as a deliberate turn end.

* Treat missing session records as unknown outcome in history retag

A transcript with no session record has no recorded outcome, so its
terminal text stays a plain row instead of getting completion styling.
2026-07-28 17:38:00 -07:00
Saoud Rizwan bd83980359 fix(vscode): also check file-backed stores before re-onboarding upgraders (ENG-2346) (#12639)
migrateWelcomeViewCompleted derived the flag solely from VS Code's
per-profile stores, which are empty for users upgrading from the live
4.x extension (file-backed config under ~/.cline/data). The flag landed
as false and fully configured users were pushed back through onboarding.

Purely additive: the existing VS Code checks are untouched; the same
signals (completed flag, provider secrets, keyless provider configs) are
now also read from the file-backed globalState.json/secrets.json and
OR-ed into the result.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 17:16:28 -07:00
Saoud Rizwan 4d3b161b9a fix(core): coerce line-number tool args (#12641)
Models sometimes emit numeric tool arguments as JSON strings. `insert_line`
and the `read_files` line bounds were plain `z.number()`, so an
`insert_line: "3"` rejected the whole tool call before it ran:

  1 tool call(s) failed: [editor] {"error":"✖ Invalid input: expected number,
  received string\n  → at insert_line"}

The model is handed that error and burns a round trip re-deriving the argument.

`z.coerce` leaves the JSON Schema advertised to the model untouched (still
`integer`), and `.int()` / `.positive()` still reject "abc", "3.5" and 3.5.
2026-07-28 16:49:01 -07:00
Saoud Rizwan 255be9ad29 fix: stop Ollama model picker polling /api/tags once per second forever (ENG-2344) (#12621)
* fix(webview): stop unbounded polling of local model endpoints (ENG-2344)

The Ollama provider form polled /api/tags every 2s from two places at once
(OllamaProvider and a dead duplicate poll in ApiOptions whose result was
never read), producing ~1 req/s for as long as the settings pane was open.
Since the base URL is user-configurable, this could hammer a remote or
metered endpoint. VSCodeLmProvider and LMStudioProvider had the same
interval pattern.

- Remove all useInterval model polling; fetch on mount and when the
  base URL changes instead
- Refresh the Ollama model list when the picker field gains focus so a
  server started after the pane opened is still discovered
- Delete the dead _ollamaModels poll in ApiOptions

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(webview): add on-demand model refresh for LM Studio and VS Code LM

Greptile review follow-up: removing the polling intervals left these two
pickers pinned to their mount-time snapshot. Mirror the Ollama picker's
interaction-driven refresh:

- LM Studio: refetch models when the model dropdown or the manual model
  id field gains focus
- VS Code LM: refetch when the dropdown gains focus, and add an explicit
  'Refresh the model list' link to the empty state

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 16:44:46 -07:00
Saoud Rizwan 77b181a472 fix(vscode): label editor insert_line edits as edits, not new-file creations (#12635)
An `editor` tool call with `insert_line` (e.g. a prepend) targets an
existing file — the SDK editor executor requires the file to already exist
for inserts — but sdkToolToClineSayTool only treated `old_text`/`replace_in_file`
as edits, so inserts were classified as newFileCreated and the approval card
read "Cline wants to create a new file:" for an existing file.

Treat insert_line as an edit so the card reads "Cline wants to edit this file:".
2026-07-28 16:30:38 -07:00
Saoud Rizwan 45476650b7 fix(vscode): stop Preferred Language from silently resetting on settings mount (#12632)
The webview-ui-toolkit VSCodeDropdown fires a spurious change event with
the wrong option (index 2, Portuguese - Brasil) while its slotted options
initialize after a window reload, and the handler persisted that value
unconditionally. Any saved language not at the top of the list could be
silently rewritten to Portuguese just by opening the General settings tab.

Replace the toolkit dropdown with the ui/select component already used by
the other settings dropdowns (Auto Compact Strategy, MCP Display Mode),
which only emits onValueChange for real user selections, and render the
options from the shared languageOptions list instead of a hardcoded copy.
2026-07-28 16:30:28 -07:00
Saoud Rizwan f847b06cfd Fix thinking indicator flashing when a turn completes (#12631)
At turn end the final message is finalized (partial: false) via the fast
partial-message stream a moment before the done event flips turnState out
of "streaming" via a full state post. During that gap the in-list
"Thinking..." loader row appeared and immediately disappeared, flashing
on every turn completion.

- Extract the loader show/hide logic from MessagesArea into a testable
  useThinkingLoaderRow hook.
- Debounce the loader when its trigger is the tail message finishing
  streaming: mid-turn a real wait outlives the grace period, while the
  turn-end phase change cancels it before it ever shows.
- Add the legacy path's say("completion_result") anti-flicker guard to
  the turnState path so attempt_completion turns never flash regardless
  of timing.
2026-07-28 16:30:18 -07:00
Saoud Rizwan 598b3af7eb fix(cli): report correct default directories for --config and --data-dir in --help (#12627)
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 16:30:00 -07:00
Saoud Rizwan fc936b7305 fix(vscode): restore Retry/Start New Task buttons after API failure (ENG-2339) (#12625)
* fix(vscode): restore Retry/Start New Task buttons after API failure

A provider stream error emits ask:'api_req_failed', but the session-event
coordinator resolved the turn-end phase to 'awaiting_followup', clobbering
the error state — so the footer never showed the error-recovery buttons and
the error surface offered no way to recover (ENG-2339).

Record the error outcome in MessageTranslatorState when the error event is
translated, and resolve turn end to the 'error' phase so the existing
api_req_failed button config (Retry / Start New Task) is reachable again,
matching legacy behavior.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): also record error outcome for done(reason:'error') terminations

A turn can terminate with done(reason:'error') without a separate 'error'
event; record the error outcome there too so turn end still resolves to the
'error' phase and the Retry / Start New Task buttons appear.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 16:29:48 -07:00
Saoud Rizwan 9247dc30bb chore(vscode): remove dead task history command (#12624) 2026-07-28 16:29:35 -07:00
Saoud Rizwan 1b499161cc Fix ignored China/international endpoint toggles for Qwen, Moonshot, Z AI (ENG-2340) (#12623)
* Fix ignored China/international API line toggles for Qwen, Moonshot, Z AI (ENG-2340)

The regional apiLine setting was persisted through both storage layers but
never consulted when resolving the request endpoint, silently sending
regional users to the wrong host.

- @cline/llms: record china/international base URLs on the builtin specs
  for qwen, qwen-code, moonshot, zai, zai-coding-plan, and minimax; expose
  resolveProviderApiLineBaseUrl; resolve options.apiLine against the
  registered apiLineBaseUrls in GatewayRegistry.createProvider (explicit
  base URLs still win).
- @cline/core: toProviderConfig now resolves the base URL from apiLine
  between the explicit setting and the static provider default.
- VS Code: buildSessionConfig resolves the API line from legacy state
  (qwenApiLine/moonshotApiLine/zaiApiLine/minimaxApiLine) with a
  providers.json fallback and forwards it on the provider config so the
  gateway can route regionally.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Share the base provider's legacy API line with qwen-code and zai-coding-plan

The coding variants have regional endpoints in the SDK but no legacy
state field of their own, so a China-line user selecting them from the
VS Code UI would silently fall back to the international default. The
variant's own providers.json apiLine still wins over the shared legacy
field.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 16:29:28 -07:00
Saoud Rizwan 0e5cb433b0 fix(core): expose live in-memory session messages for mode-switch rebuilds (#12622)
Session rebuilds seed the replacement session from readMessages, but the
persisted transcript only catches up at assistant-message/turn boundaries
and abort() does not flush. Toggling plan/act mode while a task's first
turn is mid-flight (e.g. a command approval pending) therefore rebuilt the
session with no history at all and the new mode's model lost the task.

Add RuntimeHost.readLiveSessionMessages (optional) which prefers the
resident session's agent.getMessages() and falls back to the persisted
transcript, expose it as ClineCore.readLiveMessages, and use it in the
VS Code history loader that feeds session rebuilds. readSessionMessages
keeps its persisted-transcript semantics for existing callers (compaction
validation, session snapshots, history).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 16:29:18 -07:00
Saoud Rizwan 53a46c309b Revert "Add a built-in cline-settings skill and broaden the legacy resume war…" (#12669)
This reverts commit 36c78267c6.
2026-07-28 16:15:25 -07:00
Dominic Cooney 36c78267c6 Add a built-in cline-settings skill and broaden the legacy resume warning (#12660)
* Add a built-in cline-settings skill and broaden the legacy resume
warning

Models diagnosing configuration problems have no authoritative source
for where Cline stores settings: the SDK migration removed the old MCP
documentation tool, and resumed legacy conversations can carry stale
paths and instructions from older runtimes (CLINE-2570).

Add a core-owned virtual skill, cline-settings, whose instructions are
generated at invocation from the shared storage path resolvers. It is
listed and invoked through the existing skills registry on both the
local and Hub session paths, is reserved against shadowing by
file-backed skills (case-insensitive), honors session skill allowlists
(an explicit empty allowlist disables all skills including built-ins),
and never appears in editable listRecords.

Broaden LEGACY_RESUME_MODEL_WARNING to cover stale configuration
paths, file formats, and product instructions, not just tool names.
Anchor the persisted history boundary on a stable marker; recognize
and upgrade the historical warning in place so previously resumed
tasks get the new wording without duplicate warnings, and preserve
resumed user text that shares a message with the warning.

* Fix Windows MCP stdio spawn for paths with spaces; add settings-skill
rule

The runtime-builder MCP test failed on Windows because the stdio
client spawns with shell: true there, and cmd.exe split the unquoted
executable path at the space in "C:\Program Files\nodejs\node.exe".
Quote the command and arguments for cmd.exe so any server whose
command or arguments contain spaces can start. Also raise the connect
timeout to match the request timeout: connect covers process spawn
plus the first initialize round-trip, and 1.5s is tight for cold
starts on loaded machines.

Add a brief .clinerule noting that settings/storage-path changes may
require updating the cline-settings built-in skill.

* Quote empty MCP arguments for cmd.exe

An empty-string argument passed through unquoted disappears when
cmd.exe re-parses the concatenated command line, silently shifting the
server's argument list. Quote empty values so they survive as "".

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-28 16:10:27 -07:00
Tomás Barreiro 76c30b1c60 Update ClineFreeModelLimitError wording (#12666) 2026-07-29 01:03:44 +02:00
Saoud Rizwan b496c5a1f3 chore(cli): release v3.0.47 2026-07-28 15:36:26 -07:00
Bee 2fc06ce446 feat(desktop): add pagination to session history view (#12618)
* fix(desktop): order sessions by last activity and unify status dot colors

* feat(desktop): add pagination to session history view

Display sessions in ten-item pages and fetch older history only after reaching the final local page. Add coverage for pagination, session opening, and compact token formatting.

* page numbers

* Support adding a session to favorite list

* apply feedback

* fix
2026-07-28 15:30:43 -07:00
Bee 64c50380f3 feat(chat): refine tool and reasoning message disclosures (#12616)
* feat(chat): refine tool and reasoning message disclosures

Add tool-specific icons with a fallback, display elapsed reasoning time, and restyle reasoning disclosures. Position hidden message actions outside the layout and update attachment sizing to valid Tailwind utilities.

* fix(desktop): improve chat message actions and scrolling

Refine action positioning, sizing, timestamps, and visibility for chat messages. Remove nested overflow constraints so scrolling remains controlled by the conversation viewport, and tighten tool disclosure spacing.

* tools icon mapping

* fix(desktop): align chat timestamps and tool icons
2026-07-28 15:30:33 -07:00
Saoud Rizwan e36197d911 chore(sdk): release v0.0.66 2026-07-28 15:22:13 -07:00
Bee 52d187578e feat(desktop): show subagent/teammates execution history (#12615)
* feat(desktop): expose session agent execution history

Add a list_session_agents sidecar command to retrieve agent and team run details from child sessions and tool messages. Include comprehensive tests for agent discovery, message parsing, status handling, and result normalization.

* apply feedback

* add test

* feedback fix

* fix

* fix p1
2026-07-28 15:09:27 -07:00
Tomás Barreiro a6239c420c Update generated files (#12649) 2026-07-28 13:56:05 -07:00
Bee c7c5e6518b fix(desktop): order sessions by last activity and unify status dot colors (#12617) 2026-07-28 12:06:59 -07:00
Bee 98c0717302 feat(desktop): system tray session status (#12659)
* feat(desktop): add system tray session status support

Enable Tauri tray icon and PNG image features for desktop tray integration. Expose the running session count in process context so the tray can reflect active work, with test coverage for running and idle sessions.

* fix(desktop): buffer tray actions and show app status
2026-07-28 20:59:36 +02:00
Dominic Cooney b4aed24ff8 fix(vscode): compact tasks opened from history (#12002)
* fix(vscode): compact tasks opened from history

The compact button only worked while a session was actively running.
Opening a task from history and clicking compact errored with "There is
no active task to compact."

Compaction is defined over a session transcript, so rather than grow a
second implementation for displayed tasks, resume a displayed history
task on an isolated session host and compact it through the existing
path. The coordinator owns and disposes that host, so task navigation
cannot make cleanup stop a replacement active session.

Follow-up resume and both compaction paths (idle active session and
displayed task) acquire the same session-rebuild boundary around
transcript read, session start, and persistence. Task and session
object identity are rechecked across awaits; cleanup targets only the
exact host and session started by the operation. A follow-up abandoned
by task navigation settles the streaming turn phase it pre-set, so the
newly displayed task never shows a stuck Thinking/Cancel footer.

The resume-start preparation shared by follow-up and compaction is
extracted into prepareTaskResumeStartInput, including legacy task
conversion, so the two callers cannot drift apart.

The compaction divider UX and context-meter shrink remain owned by the
already-merged webview compaction change.

* fix(vscode): deliver follow-ups across a same-task proxy reload

Follow-up targeting checks compared the displayed TaskProxy by object
identity, but showTaskWithId allocates a fresh proxy for the same task
id, so reloading the task mid-resume silently dropped the message.
Compare targeting by taskId; cleanup keeps object identity.
2026-07-28 10:52:13 -07:00
Saoud Rizwan e9ec82d2ef docs: remove stray double space in README CLI example (#12594)
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 09:45:51 -07:00
Saoud Rizwan e3ff875e09 fix(cli): persist /settings general toggles (mode, auto-approve, compaction) across restarts (#12614)
* feat(core): persist plan/act mode, tool auto-approve, and compaction mode in global settings

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): restore /settings general toggles across restarts

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): make global settings updates cross-process safe

Targeted setters previously did unlocked read-modify-write cycles over the
shared global-settings.json, so concurrent hosts (two CLIs, or CLI + VS Code)
could silently discard each other's changes. Route all setters through a new
updateGlobalSettings(mutate) helper that re-reads the latest on-disk state
under a short-lived lock file (with stale-lock reclaim and a bounded wait)
and replaces the file atomically via temp-file rename so readers never see
torn writes.

* Revert "fix(core): make global settings updates cross-process safe"

This reverts commit 198c1c831b.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-28 08:56:55 -07:00
Tomás Barreiro dc175c73a8 Cline free ux (#12593)
* Introduce the concept of free models that have the cline-free ID

* Add (free) to explicitly free models

* Render (free) in free models name

* Add pricing to the free model info

* Fix free model pricing

* fix tests

* SEt cline-free model pricing to 0

* revert pricing changes

* Add free limit error handling

* Include the reset time in the message

* Add button to switch model in VSCode

* add model not found error

* remove problematic tests

* fix review messages

* Fix model promotion ended

* Revert "revert pricing changes"

This reverts commit 7e5b2a34fd.
2026-07-28 06:20:28 +02:00
Tomás Barreiro d91f1ce166 Add support for cline-free models. (#12591)
* Introduce the concept of free models that have the cline-free ID

* Add (free) to explicitly free models

* Render (free) in free models name

* Add pricing to the free model info

* Fix free model pricing

* fix tests

* SEt cline-free model pricing to 0
2026-07-28 05:50:15 +02:00
Saoud Rizwan bc5a2e85a4 chore(desktop): release v0.0.6 2026-07-27 19:01:44 -07:00
Saoud Rizwan 9c56a726a7 Add persistent sidebar update indicator for staged desktop app updates (#12611)
* Add persistent sidebar update indicator for staged app updates

* Surface restart failures from the update indicator and reset its pending state
2026-07-27 18:37:51 -07:00
Saoud Rizwan 72c9bbfcaa Clarify desktop app auto-update setting: it governs the CLI, not the app (#12601)
* Reword desktop auto-update setting description to drop CLI mention

* Clarify desktop settings copy: auto-update toggle governs the CLI, not the app
2026-07-27 18:14:39 -07:00
Bee 53a7c3e80e fix(desktop): align startup appearance and session context (#12608)
* fix(desktop): align startup appearance and session context

* update cline logo size

* display full workspace name

* fix: fits in narrow screen size

* header in narrow screen

* Transient failures no longer replace a valid branch with no-git.

* header alignments

* account settings button row

* fix(desktop): improve collapsed sidebar settings layout

Use a compact overlay-friendly width and left-align navigation controls in collapsed settings. Adjust header padding, stack account details, anchor the expand button, and add layout regression tests.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-27 18:13:55 -07:00
John Choi 8751daaba9 refactor(ui): extract desktop session status (#12596) 2026-07-27 17:16:04 -07:00
Bee aa1fffbe80 fix(ui): use solid primary color for checked switch (#12600)
Update the switch's checked state background from translucent to solid primary for clearer visual feedback and improved contrast.
2026-07-27 16:55:43 -07:00
Saoud Rizwan 283c3ba937 ci: grant pull-requests write so promo-comment deletion works on PRs (#12606) 2026-07-27 16:51:00 -07:00
Saoud Rizwan 69c9a9ac28 ci: delete coding-agent promo comments on PRs (#12604) 2026-07-27 16:44:48 -07:00
Bee 4d238558d0 feat(deaktop): queue ui update (#12534)
* feat(deaktop): queue ui update

* fix(desktop): address queue review feedback

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-27 16:06:16 -07:00
Bee 51e5daf8eb feat(core): persist and restore connector sessions (#12125)
* feat(core): persist and restore connector sessions

Persist successful connector runs for autostart and disable persistence when connectors are stopped. Reconnect saved connector channels during CLI daemon or hub startup so previously connected adapters are restored after restarts without blocking startup.

* fix(core): harden connector autostart recovery

* fix(core): address connector autostart edge cases

* fix(core): address connector reconnect review feedback

* fix(core): address remaining connector review feedback

* fix(core): address connector persistence review feedback

* fix(core): address connector lifecycle review feedback

* fix(core): restart surviving connectors after hub restart

* fix(core): refresh Telegram identity after token rotation

* fix(hub): restart active connectors on start

* fix(connectors): address lifecycle review regressions

* fix(connectors): make reconnects instance-safe

* fix(connectors): harden restart failure handling
2026-07-27 15:59:23 -07:00
John Choi 28ef954258 feat(ui): add host-safe theme and Markdown exports (#12439)
* feat(ui): add host-safe theme and package contract

* fix(ui): preserve scoped theme contract

* refactor(ui): establish generated theme contract

* chore(ui): refresh committed build

* fix(ui): isolate markdown presentation

* chore(ui): prepare next package preview

* fix(ui): harden preview package contract

* fix(ui): enforce clean package builds in CI

* refactor(ui): rely on npm package builds

* test(ui): make package smoke failures actionable

* ci(sdk): restrict pull requests to main

* test(ui): verify the published package contract

* docs(ui): document the React types floor

* chore(ui): keep package checks out of shared workflows

* fix(ui): preserve standalone markdown cascade

* fix(ui): reject stale generated theme builds

* refactor(ui): narrow foundation to adoption needs

* test(ui): verify packed CSS exports exist

* fix(ui): restore publish contract safeguards

* refactor(ui): keep foundation adoption-focused

* test(ui): verify new packed CSS exports
2026-07-27 15:26:27 -07:00
Saoud Rizwan 9c4841ea07 chore(desktop): release v0.0.5 2026-07-27 15:21:49 -07:00
Saoud Rizwan 1bd200e906 ci: strip cloud-agent promo wrappers from PR bodies (#12588)
* ci: strip cloud-agent promo wrappers from PR bodies

* ci: make PR body strip vendor-agnostic, pin github-script to SHA
2026-07-27 15:10:05 -07:00
Saoud Rizwan fabbc144d6 perf(desktop): make the app feel snappy end-to-end (#12568)
* perf(desktop): make the app feel snappy end-to-end

Fixes several compounding sources of UI jank that made every click and
keystroke feel seconds-slow:

- Aurora background: drop per-frame 46-64px CSS blur re-rasterization;
  bake softness into gradients + a static mask and animate only
  opacity/transform (compositor-only). Onboarding/home idle went from
  ~10fps to a locked 60fps under 4x CPU throttling.
- Hide the app shell while the opaque onboarding overlay is up so a
  second aurora + hero animations are not composited underneath.
- Hero verb animation: opacity/transform only (no text blur filter).
- Composer: keystroke state now lives inside ChatInputBar (versioned
  promptDraft injections for quick actions/undo/resets), and mention/
  slash detection is derived instead of effect-synced; typing went from
  245/246 keystrokes over 50ms to 3/240.
- Chat streaming: coalesce per-token text/reasoning deltas into ~48ms
  flushes; memoize MessageBubble/ToolMessageBlock with stable callbacks
  so finished messages skip re-rendering during streams.
- Session history: only surface isLoadingHistory before the first load;
  background refreshes no longer re-render the whole app twice each.
- Provider catalog (~700KB): dedupe concurrent fetches with a short TTL
  so app boot issues one round-trip instead of three.
- Sidecar: session-log appends are now ordered async writes instead of
  writeFileSync per streamed token; git/folder-picker/editor discovery
  use async execFile so the native picker no longer freezes every
  pending command; editor discovery results cached for 60s.

* fix(desktop): address Bugbot review findings

- Invalidate the shared provider-catalog cache after any provider
  mutation (onboarding connect paths, account sign-in/out, settings
  save, add provider) so post-save reloads never see a pre-save copy.
- Clear the injected composer draft on send so a composer remount
  cannot repopulate the previous prompt.
- Mark the hidden app shell inert + aria-hidden while the onboarding
  overlay covers it, keeping covered controls out of the tab order.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-27 15:04:49 -07:00
Amy Duquette 372f029343 docs: add Poolside provider setup guide (#12586) 2026-07-27 21:38:35 +02:00
Tomás Barreiro 9d9b7aa6f8 Add data dirs when running specific environments (#12585) 2026-07-27 21:19:07 +02:00
Dominic Cooney 8c7095b805 test(cli): avoid hard-coded dialog background color (#12579)
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-07-27 11:07:54 -07:00
Dominic Cooney 05535e844c chore(vscode): remove unused OpenTelemetry dependencies (#12573)
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-07-27 10:29:13 -07:00
Tran Binh Minh c3dc4a95bc fix(cli): use unique keys for read_files rows (#12144)
read_files rendered file rows keyed by raw path, so the same path listed twice (e.g. find-skills reading a SKILL.md repeatedly) produced duplicate React keys and the two-children-with-the-same-key warning. Build index-namespaced keys instead, at both render sites.

Fixes #9784

Signed-off-by: Minhkunn <minh.12072k6@gmail.com>
Co-authored-by: Minhkunn <minh.12072k6@gmail.com>
2026-07-26 13:41:30 -07:00
Sufiyan Khan 9466fcc018 docs: fix typos, incorrect slash command, and hooks vs plugins mismatch (#12154)
* docs: fix typos and incorrect slash command reference

- Fix double period in MiniMax provider description
- Remove duplicate 'through' in kanban install description
- Fix /new -> /newtask (correct slash command name)

* docs(hooks): fix description to reference SDK Plugins, not SDK Hooks

The description said 'SDK Hooks page' but the content links to the
SDK Plugins page (/sdk/plugins). Align the description with the
actual destination.
2026-07-26 13:13:39 -07:00
Saoud Rizwan dd7a1c5fa6 fix(desktop): onboarding Cline API key path, stuck "Agent is working..." composer, OAuth sign-in cancel (#12564)
* fix(desktop): clear busy status when queued turns finish; add Cline API key onboarding path

* feat(desktop): allow cancelling a pending Cline browser sign-in during onboarding

* fix(desktop): address review findings on OAuth cancel, API key verification, and queued-turn status

* fix(desktop): cancel pending OAuth logins when the initiating transport connection closes

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-26 02:10:15 -07:00
Saoud Rizwan fe75eb6271 fix(vscode/core): agentic compaction silently fell back to basic, and manual /compact never reached the model (#12563)
* fix(vscode): resolve base URL and knownModels for compaction summarizer

The agentic compaction summarizer creates its LLM handler from the
session's ProviderConfig alone. For the OpenAI Compatible provider
stored under its SDK spelling (openai-compatible), resolveBaseUrl had
no mapping, so ProviderConfig was built without a baseUrl and the
summarizer silently hit the provider default endpoint (api.openai.com),
failed auth, and fell back to basic compaction - the UI still showed
'Context compacted' with no hint that agentic summarization never ran.

- resolveBaseUrl: accept the SDK spelling of the OpenAI Compatible
  provider, and fall back to the providers.json base URL (mirroring
  resolveApiKey) when legacy state has none.
- buildSessionConfig: expose knownModels at the top level of
  CoreSessionConfig, so manual compaction (sdk-compaction.ts) budgets
  against the real model context window instead of the 64k fallback.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): project compaction sidecar even when auto-compaction is disabled

Manual /compact persists a compaction sidecar and promises the next turn
will use the compacted working context, but the runtime host only wired
the compaction-state-aware prepareTurn when compaction was enabled. With
Auto Compact off (the VS Code extension default), a manual /compact was
a silent no-op for the model: the sidecar was saved and the UI showed
'Context compacted', yet every subsequent request still sent the full
canonical transcript.

createCompactionStateAwarePrepareTurn already supports an undefined
compact fn (project existing state, never re-compact), so wire it
unconditionally; sessions without a sidecar are unaffected. Also keep a
resumed/initial sidecar instead of dropping it when compaction is
disabled.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-26 01:20:08 -07:00
Saoud Rizwan e4091c3686 Stop the task at the mistake limit like the CLI and remove the max-mistakes setting (#12561)
* fix(vscode): stop the task at the mistake limit like the CLI, drop the max-mistakes setting

When the SDK's consecutive-mistake limit is hit, the extension used to
block on an ask (Proceed Anyways / Start New Task) while the agent loop
kept running against the provider — reproduced 2,100+ consecutive API
requests behind the unanswered prompt.

Replicate the CLI's non-interactive resolver instead: show an error row
and resolve the decision as an immediate stop. The run aborts cleanly at
the turn boundary, the turn phase becomes awaiting_followup, and the
user continues whenever they want by sending a new message (which also
resets the SDK's mistake tracking on the next productive turn).

Also remove the extension's maxConsecutiveMistakes setting (state key,
settings RPC, webview state, proto fields now reserved). It was never
wired into the SDK session config — the SDK's own default governs — so
the setting was dead weight. Legacy mistake_limit_reached asks from
persisted conversations still render via the existing webview paths.

* fix(proto): reserve retired Settings field 139 (max_consecutive_mistakes)

The original removal added 'reserved 139' but the proto generator at the
branch base had no reserved-statement support and silently dropped it on
regeneration. Main (b4c640733) taught generate-state-proto.mjs to
preserve reserved statements, so after the merge the reservation now
survives. Also reserve the field name, mirroring the custom_prompt
removal pattern.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-26 00:53:41 -07:00
Saoud Rizwan bcf8c1d03e fix(vscode): relative read_files paths and diff-preview stalls no longer fail read/edit tools (#12558)
* fix(core): resolve relative read_files paths against the session cwd

The built-in FileReadExecutor resolved relative paths against process.cwd(),
which in a VS Code extension host is typically '/' rather than the workspace.
Every relative-path read failed with ENOENT, so models fell back to reading
files through the terminal. Resolve relative paths against the tool's
configured cwd in createReadFilesTool before invoking the executor.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): never let a stalled diff preview open fail or delay an edit

Thrown errors from opening the edit diff preview were already swallowed, but
a hung vscode.diff call was unbounded: on auto-approve it burned the editor
tool's 30s execution timeout (failing the whole edit), and on manual approval
it delayed the approval ask indefinitely. Bound the preview open with a 5s
timeout; on timeout the edit proceeds without a preview and the late-opening
tab is closed once the open settles.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style(core): order node:path import first for biome

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): flatten the preview-open timeout into a plain race

Replace the custom timeout error class, the race helper with timer
bookkeeping, and the two-branch cleanup with a single Promise.race and one
settle-then-close line. Same behavior: a rejected or stalled preview open
never blocks the approval ask or fails the edit, and any late-appearing tab
is closed once the open settles.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): move the read_files cwd fix out of the SDK into the host

Revert the SDK change and instead override the read_files executor in the
extension, alongside the existing editor/apply_patch/askQuestion overrides.
The override resolves relative paths against the workspace root before
delegating to the SDK's built-in reader, since the extension host's
process.cwd() is usually '/' and every relative-path read failed with ENOENT,
pushing the model into terminal fallbacks. The SDK is left untouched.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-26 00:22:29 -07:00
Saoud Rizwan 5099eed5ee fix(desktop): consistent MCP server cards and single uninstall affordance (#12555)
* fix(desktop): consistent installed cards and single uninstall in marketplace views

* fix(desktop): surface marketplace setup guidance on matched installed cards

* fix(desktop): show setup guidance for all matched marketplace entries, not just first match

* fix(desktop): unambiguous entry-to-item matching and no stale installed card flash

* fix(desktop): drop orphaned installed keys optimistically instead of hiding cards during recheck

* fix(desktop): guard recheck races and avoid duplicate uninstall for ambiguous matches

* fix(desktop): keep uninstall action on ambiguous fallback marketplace cards

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-26 00:12:28 -07:00
Saoud Rizwan b4c640733b Remove dead "Use compact prompt" toggle from LM Studio provider settings (#12551)
* Remove dead 'Use compact prompt' toggle from LM Studio settings

The compact system prompt option was never wired up in the SDK-based
extension: the customPrompt value was stored in state and echoed back
to the webview, but nothing in the session factory or SDK ever read it
to alter the system prompt. Remove the checkbox (only shown for the
LM Studio provider) and all the dead state/proto plumbing behind it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add changeset for compact prompt toggle removal

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reserve removed custom_prompt field number/name in Settings proto

Teach generate-state-proto.mjs to preserve reserved statements in the
generated Secrets/Settings messages so removed fields keep their wire
identity reserved across regenerations, and reserve field 150 and the
custom_prompt name (plus the name in UpdateSettingsRequest).

Addresses Greptile review feedback on #12551.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Never assign reserved proto field numbers to new Settings fields

If the highest-numbered field was removed and reserved, the generator
would hand that same number to the next new field, emitting both a
reserved statement and a live field at the same number. Parse reserved
numbers (including ranges) from the existing message, skip them when
assigning new numbers, and fail fast if an active field collides with
a reservation.

Addresses Bugbot review feedback on #12551.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Format generate-state-proto.mjs

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-25 22:57:10 -07:00
Saoud Rizwan 40ba9d25eb fix(webview): use VS Code theme selection colors in dropdown menus (#12554)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-25 22:54:45 -07:00
Saoud Rizwan adb8ee0c4d fix(vscode): model ID under chat field stale / resets to gpt-4o for OpenAI Compatible (#12552)
* fix(vscode): fold SDK openai-compatible provider id to legacy openai spelling

The settings provider dropdown sources ids from the SDK catalog, so picking
OpenAI Compatible stored 'openai-compatible' into plan/actModeApiProvider.
Every provider-keyed code path (webview model label, planModeOpenAiModelId
slots, session factory) expects the legacy 'openai' spelling, so the model
id under the chat field went stale after Done and fell back to the catalog
default (gpt-4o).

- parseProviderId + toLegacyApiProvider now alias openai-compatible -> openai
- state-keys load transform migrates already-stored SDK spellings
- convertProtoToApiProvider normalizes provider ids written from the webview
- commitModelSelection writes the legacy spelling and posts state to the
  webview so model-only commits refresh the chat model label immediately
- session factory normalizes provider ids from state and providers.json

* fix(vscode): make toLegacyApiProvider alias lookup case-insensitive

parseProviderId lowercases before its alias lookup, but toLegacyApiProvider
(used directly by convertProtoToApiProvider and the state-keys load
transform) matched aliases case-sensitively, so a mixed-case
'OpenAI-Compatible' would not fold. Fall back to a lowercased lookup while
preserving original casing for unknown ids.

* fix(vscode): treat spelling-only provider differences as the same provider

Addresses the Bugbot finding on PR #12552: stale snapshots can still hold
the SDK spelling (openai-compatible) while new writes use the legacy
spelling (openai). Normalize both sides of the provider comparisons in
SdkProviderChangeCoordinator.providerForMode and
SdkController.isSelectionForActiveModeProvider so a spelling-only
difference neither restarts the active session nor skips the lightweight
in-session model update.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-25 22:53:58 -07:00
Saoud Rizwan 01617f9a05 chore: remove orphaned legacy auto-retry UI and dead onRetryAttempt plumbing (#12557)
The pre-SDK extension auto-retried failed API requests and surfaced
'Auto-retrying in X seconds' rows (say:'error_retry') plus a retryStatus
header on api_req_started. The SDK-based extension never emits either:
errors map straight to an api_req_failed ask with a manual Retry button,
and retrying is handled silently by the AI SDK / auth-refresh retry.

Remove the orphaned webview rendering (ChatRow error_retry case,
ErrorBlockTitle, combineErrorRetryMessages, isRequestInProgress chain,
stories), the unused say types and proto enum values (reserved), the
retryStatus field, and the never-invoked onRetryAttempt callback from
ApiHandlerOptions, sdk-api-handler, and @cline/llms provider config.

Legacy transcripts may still contain error_retry / api_req_retried rows;
readUiMessages now drops them so old tasks don't render raw JSON.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-25 22:52:11 -07:00
Saoud Rizwan b72d7f1a6f fix(vscode): keep webview alive when moved between primary and secondary sidebars (#12553)
* fix(vscode): keep webview alive when moved between sidebars

Moving the Cline view between the primary and secondary sidebars made the
view go blank with 'this.unsubscribeHostTelemetrySettings is not a function'.

Two fixes:
- The vscode host bridge streaming client returned the async IIFE's Promise
  instead of the cancel function its contract declares, so callers invoking
  the stored unsubscribe function threw a TypeError. It now returns a
  synchronous wrapper that resolves the real cancel function in the background.
- VscodeWebviewProvider disposed the whole Controller on WebviewView
  onDidDispose. VS Code destroys and re-resolves the view when it is moved
  between sidebars, so the re-resolved view was served by a dead controller
  (postStateToWebview no-ops after dispose) and rendered blank. onDidDispose
  now only releases view-scoped resources; the controller is disposed on
  extension deactivation via WebviewProvider.disposeAllInstances.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): address review — don't clear active task on re-resolve, guard stale view dispose

- resolveWebviewView no longer calls clearTask on re-resolves (moving the
  view between sidebars must not terminate a running task); it only clears
  stale task state on the first resolve after activation.
- onDidDispose now only tears down view resources if the disposed view is
  still the active one, so a stale dispose event arriving after a newer view
  resolved cannot clobber the active view's listeners. resolveWebviewView
  also releases the previous view's resources up front.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-25 22:47:40 -07:00
Saoud Rizwan d5b966d0a8 fix: center-align sign-in verification code box in chat (#12533)
The 'Enter this code in your browser' box shown after clicking
'Sign in to Cline' was left-aligned while the surrounding logged-out
message and button are centered. Center the label and the code/copy row.

Fixes #12531

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-07-25 18:48:45 -07:00
Sufiyan Khan 1bb0833287 fix(cli): isolate hub daemon abort handling (#12500) 2026-07-24 18:44:51 -07:00
Saoud Rizwan 2c64c4ce5b fix: OpenAI Compatible model list fails when base URL has a trailing slash (#12532)
* fix: normalize trailing slash in OpenAI Compatible base URL for model list fetch

* fix: construct OpenAiModelsRequest via proto create in refreshOpenAiModels test

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-24 15:28:51 -07:00
Bee 0bdf2e3468 fix(desktop): restore window dragging (#12529) 2026-07-24 22:07:34 +02:00
Dominic Cooney 98024c4243 fix(cli): patch @opentui-ui/dialog for opentui 0.4.x remove() contract (#12516)
@opentui-ui/dialog@0.1.2 is built against @opentui/core ^0.1.69, whose
Renderable.remove(id) took a string id. Core 0.4.x renamed it to
remove(child) and throws when handed anything but a renderable, so the
dialog package's removeDialog()/provider teardown aborted before
detaching the panel: the React portal content unmounted but the
imperative grey box stayed on screen over the chat after every dialog
close (model picker, help, command palette, ...).

The upstream package is abandoned at 0.1.2, so pin the fix with a bun
patch that passes the renderable object on all three bindings (react,
solid, core container). A tui-test opens and dismisses the help dialog
and asserts the panel's #262626 background is fully gone, not just its
text.

Fixes #12506

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-07-24 15:18:15 +09:00
Saoud Rizwan 359e771ab3 Set up Cloud dev environment (CLI, VS Code extension, desktop app) (#12515)
* docs: add Cursor Cloud dev environment setup notes (AGENTS.md)

* docs: document VS Code extension + desktop app dev setup (AGENTS.md)

* docs: trim AGENTS.md cloud agent instructions

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-23 19:26:10 -07:00
Saoud Rizwan a21b21de84 chore(desktop): release v0.0.4 2026-07-23 18:50:17 -07:00
Bee ca6f6a6c23 feat(desktop): use the shared Cline Hub runtime (#12508)
* feat(desktop): use the shared Cline Hub runtime

* fix(hub): group code-sidecar-observer clients under Code App

The desktop observer client type was renamed from code-sidecar-approvals
to code-sidecar-observer, but the Code App grouping matchers in the hub
dashboard and menubar sidecar still only matched the old type. Since the
observer now registers on the shared Hub, it showed up as a separate
ungrouped client. Keep the old type matched for older desktop builds.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-23 18:21:39 -07:00
Bee 3e06abc366 feat(desktop): support chat without workspaces (#12412)
* feat(core): support pathless sessions with temporary workspaces

* fix(desktop): mark editor icons as decorative

* fix(core): omit absent auth request IDs

* test(sdk): restore request_id auth telemetry param in core-events test

The branch's drive-by request_id -> requestId rename was dropped while
resolving the merge conflict with #12444 (which added requestIdDetails on
main), so the public captureAuthLoggedOut/captureAuthRefreshSoftFailure
API keeps its original parameter name.

* refactor(sdk): root pathless session workspaces under the cline data dir

Move the workspace created for pathless session starts from
<os.tmpdir()>/cline/sessions/<id>-temp/project to
<cline-data-dir>/workspaces/<id>/project (default
~/.cline/data/workspaces/<id>/project), per PR review:

- OS tmp reapers (macOS ~3-day purge, systemd-tmpfiles, reboot cleanup)
  silently delete user work created in 'New Project' sessions
- /tmp is a shared namespace on Linux: the first user to create /tmp/cline
  owns it (EACCES for everyone else), and guessable session IDs let a local
  attacker pre-create the workspace directory
- under the data dir the workspace shares the session store's lifecycle and
  the existing CLINE_DATA_DIR / CLINE_DIR overrides for tests and sandboxes

isTemporaryWorkspacePath now matches the .cline/data/workspaces/<id>/project
segment shape, and the -temp suffix is gone since the id-scoped directory no
longer needs to mark itself as reapable.

* feat(sdk): open pathless sessions in one shared chat workspace

Instead of minting a workspace directory per session
(<data>/workspaces/<session-id>/project), all sessions started without a
cwd/workspaceRoot now share <cline-data-dir>/workspaces/chat (default
~/.cline/data/workspaces/chat). Starting a pathless session seeds the
directory with an AGENTS.md rules file (only when missing, so users can
edit it) that tells the agent to treat the session as a chat: don't create
or edit files unprompted, ask where a project should live when the user
wants one built, and default to a new named folder inside the chat
directory that later sessions can reference.

This avoids unbounded per-session directory sprawl, gives chat sessions a
stable home the user can revisit, and groups them naturally in the desktop
sidebar. The desktop app now labels the shared workspace "Chat" (menu
action "Just chat") instead of "New Project", and isChatWorkspacePath
matches only the chat directory itself, so project folders created inside
it behave as regular workspaces.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-23 18:09:03 -07:00
Saoud Rizwan 6518b05b6c feat(desktop): first-run onboarding flow (#12495)
* feat(desktop): add replay new-user-experience setting

Adds an onboarding state module (localStorage-backed, with a reset event
the app shell can subscribe to) and a 'New user experience' row in
Settings -> General with a Replay button so beta testers and designers
can re-run the first-run flow. The flow itself ships in the stacked
follow-up PR.

* feat(desktop): first-run onboarding flow

Full-screen first-run experience shown until completed once: a welcome
step (3D glass logo over the aurora background), a connect step offering
Cline sign-in (recommended) or bring-your-own API key against the
provider catalog, and a done step that drops the user into a fresh
thread. Completion is tracked by the onboarding state module from the
previous PR; the Settings replay row now re-enters the flow in place via
the reset event, so its toast is gone. Skipping is always available so
nobody gets trapped; the connected provider (and its default model when
known) is remembered so the chat composer opens pointed at it.

* fix(desktop): address greptile review on onboarding flow

- Filter the bring-your-own-key picker to providers a lone API key can
  fully configure: providers with structured config fields (Vertex gcp.*,
  Bedrock aws.*) or no API-key field at all (Claude Code) no longer appear,
  since connecting them here would report success without working.
- Record Cline as the active provider when a signed-in user hits Continue,
  so replaying onboarding doesn't leave the chat pointed at a previously
  selected provider.

* feat(desktop): accent color themes and switchable app icon (#12496)

* feat(desktop): accent color themes and switchable app icon

Settings -> General grows an appearance cluster next to Dark mode:

- Accent color: six palettes from the Figma exploration (violet default,
  graphite, cyan, pink, espresso, ember). Non-default accents re-anchor
  --primary/--primary-foreground/--primary-emphasis/--ring per light and
  dark mode via html[data-cline-accent] overrides in globals.css, tuned in
  OKLCH to mirror the brand token relationships; chart and sidebar tokens
  alias var(--primary) so they follow. Persisted in localStorage and
  applied at boot alongside the dark-mode sync.

- App icon: the four Figma variants (Classic, Sunrise, Steel, Midnight).
  The webview persists the choice, swaps the favicon in browser mode, and
  in the Tauri shell calls the new set_app_icon native command, which
  loads the matching bundled resource (icons/dock/*.png) and applies it
  via NSApplication.applicationIconImage on the main thread. macOS resets
  the dock icon every launch, so the shell re-applies the stored choice at
  boot; classic is also loaded from a resource because the objc2 binding
  warns against passing nil to restore the bundled icon. Other platforms
  no-op (Ok(false)).

* fix(desktop): don't let a stale app-icon failure roll back a newer selection
2026-07-23 16:59:50 -07:00
Bee f9f492319e feat(desktop): redesign channel setup as expandable cards (#12490)
* feat(desktop): redesign channel setup as expandable cards

Replace the add-channel dialog with inline expandable configuration forms, including per-channel validation and error handling.

Add comprehensive tests for channel configuration, conditional fields, security options, and connection workflows.

* Connect

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-23 16:54:29 -07:00
Saoud Rizwan c3ab557194 polish(desktop): cleaner chat markdown + external links that actually open (#12497)
* polish(desktop): cleaner chat markdown + external links that actually open

Links in chat markdown never opened in the packaged app: the confirm
dialog's window.open(_blank) is silently dropped by the Tauri shell.
Route opens through openExternalUrl (open_external_url sidecar command)
and only keep the confirmation dialog for deceptive links whose visible
text reads as a URL on a different host than the real destination —
ordinary external links now open directly in the default browser.

Visual pass on Streamdown output for the chat pane: collapse the
double-boxed code block card and drop the language header row, reveal
the copy button on hover only, turn off line numbers, single-box tables,
chat-scale the heading ramp (h1 was text-3xl next to 14px body), outside
list markers, and tighter block rhythm.

* fix(desktop): harden deceptive-link detection per review

Recurse into element children when extracting link label text so inline
formatting (e.g. a bolded hostname) can't dodge the deception check, and
compare port and (when the label states one) scheme in addition to
hostname so same-host links to an unexpected scheme or port still get
the confirmation dialog. An unparseable destination behind URL-shaped
label text is now treated as deceptive rather than waved through.

* fix(desktop): treat trailing-dot FQDN labels like their plain hostname

Browsers resolve 'github.com.' identically to 'github.com', but the
URL-shaped-label pattern rejected the trailing dot, so a deceptive label
like [github.com.](https://evil.example) skipped the deception check and
opened directly. Accept one trailing dot in the pattern and strip
trailing dots during hostname normalization on both sides, so the FQDN
form is deceptive exactly when the plain form is.

* fix(desktop): treat protocol-relative labels like their https form

A label spelled '//github.com' reads as a URL but failed the URL-shaped
pattern (which only tolerated an https?:// prefix), so it skipped the
deception check and opened an unrelated destination directly. Accept a
protocol-relative prefix in the pattern, and parse '//'-prefixed values
as https-relative in parseLinkParts — prepending 'https://' to them
produced an empty hostname and made the comparison a no-op.
2026-07-23 16:48:50 -07:00
Bee 9fa37a9128 feat(desktop): display image attachments in chat (#12502)
* feat(desktop): drag and drop files to attach them to the chat

The Tauri webview swallows OS file drags by default (dragDropEnabled),
so HTML5 drop events never fire. Disable it on the main window per the
Tauri v2 docs, then handle standard dragenter/dragover/dragleave/drop on
the chat pane: dropped files feed the same dedupe-and-append pipeline as
the paperclip picker, with a depth-counted 'Drop to attach' overlay while
files are dragged over. Image drops become data-URL images via the
existing serializeAttachments path.

* feat(desktop): display image attachments in chat

* 225x225

* fix(desktop): preserve queued attachments

* fix(desktop): distinguish queued image turns

* fix pending

* fixed

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-23 16:46:36 -07:00
Bee 9cc7796bbe feat(desktop): add custom overlay title bar navigation (#12504)
* feat(desktop): add custom overlay title bar navigation

Configure Tauri to use a hidden overlay title bar and host back/forward navigation in the draggable sidebar header. Preserve agent title width during editing to prevent layout shifts, with tests covering both behaviors.

* fix(desktop): reconcile deleted navigation entries

* fix(desktop): dedupe session deletion events

* fix(desktop): serialize session deletion state

* fix(desktop): use exported DesktopAppView type in page.tsx

AppView is a non-exported type local to agent-sidebar.tsx, so referencing
it in page.tsx was a TS2304 error hidden by the typecheck script's webview
exclusion and next's ignoreBuildErrors.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-23 16:46:07 -07:00
Saoud Rizwan 439baee17c feat(desktop): add replay new-user-experience setting (#12494)
Adds an onboarding state module (localStorage-backed, with a reset event
the app shell can subscribe to) and a 'New user experience' row in
Settings -> General with a Replay button so beta testers and designers
can re-run the first-run flow. The flow itself ships in the stacked
follow-up PR.
2026-07-23 16:43:33 -07:00
Bee 469debdb30 fix(schedules): default headless routines to yolo (#12489)
* fix(schedules): default headless routines to yolo

Centralize the Cline default model ID in @cline/shared while preserving the @cline/llms export. Keep explicit modes stable and disable ask_question for unattended scheduled runs.

* autoapprove

* fix unit test

* fix(schedules): harden headless routine execution

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-23 14:53:03 -07:00
John Choi 79589c8417 test(cli): relax cold history dispatch timeout (#12511) 2026-07-23 14:36:33 -07:00
Saoud Rizwan 8e5471de9c feat(vscode): show compaction progress and results in the webview (#12487)
* feat(vscode): show compaction progress and results in the webview

Port the CLI's compaction UX to the VS Code extension:

- Translate the SDK's compaction status notices into a say:'compaction'
  divider row with a spinner while running, updated in place (same ts) to
  'Context compacted · x → y tokens · n → m messages' when done, matching
  the CLI's divider. Dangling dividers finalize as failed/cancelled when
  the turn errors or ends mid-compaction.
- Manual /compact (button or slash command) drives the same divider from
  the compaction coordinator instead of plain info lines, capturing token
  counters from the SDK's status notices.
- Drop raw status-notice slugs ('auto-compacting') that previously
  rendered as info rows.
- Context window bar now reads the compacted size (tokensAfter) from a
  compaction row newer than the last API request, so it drops immediately
  after compaction instead of waiting for the next turn.
- Fix the Auto Compact Strategy selector showing 'basic' when unset; the
  effective default is agentic (core defaults strategy ?? 'agentic').

* fix(vscode): apply compaction shrink as a ratio to the context meter

Address review feedback from #12487:

- getLastApiReqTotalTokens: instead of substituting the compaction
  notice's tokensAfter (an SDK estimate on a different scale than
  provider-reported usage, which made the bar re-snap when the next
  request's real usage landed), scale the last provider-reported request
  total by the compaction's tokensAfter/tokensBefore ratio. Both
  counters come from the same estimator, so the ratio is scale-free.
  Multiple compactions since the last request compound. A completed
  divider without token counters leaves the total unscaled.
- Suppress only the known-internal status notices explicitly
  (compaction-budget-adjusted); an unrecognized status notice now falls
  through to an info row so future notices surface instead of silently
  vanishing.
- Cross-reference the two compaction-divider finalization paths (auto:
  translator finalizeDanglingCompaction; manual: coordinator catch) so
  terminal-state rule changes touch both.
- Post state to the webview before re-throwing in the coordinator's
  failure path, consistent with the other terminal branches.
2026-07-23 10:07:57 -07:00
Saoud Rizwan cd553d2343 feat(desktop): drag and drop files to attach them to the chat (#12498)
The Tauri webview swallows OS file drags by default (dragDropEnabled),
so HTML5 drop events never fire. Disable it on the main window per the
Tauri v2 docs, then handle standard dragenter/dragover/dragleave/drop on
the chat pane: dropped files feed the same dedupe-and-append pipeline as
the paperclip picker, with a depth-counted 'Drop to attach' overlay while
files are dragged over. Image drops become data-URL images via the
existing serializeAttachments path.
2026-07-23 10:06:16 -07:00
Mikołaj Kondratek 7b776225b9 fix(telemetry): report host identity on SDK-pipeline events (#12503)
* fix(telemetry): report host identity on SDK-pipeline events

On JetBrains standalone cline-core, SDK-pipeline events (task lifecycle,
token usage, tool usage, provider failures) reported the hardcoded
cline_type "VSCode Extension", platform "VS Code", and
platform_version "unknown", unlike the classic TelemetryService which
resolves these from HostProvider.env.getHostVersion().

Extend the host_plugin_version resolution in VscodeTelemetryPolicyService
to apply the full host identity (cline_type, platform, platform_version)
with the same mapping the classic pipeline uses, before the telemetry
gate opens. Fields the host does not report keep the construction-time
fallbacks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): fall back to unknown host identity, not VSCode labels

A failed getHostVersion lookup previously left the hardcoded VSCode
identity in place, hiding the failure as a plausible-looking row.
"unknown" makes the failure visible and matches the classic
TelemetryService's || "unknown" semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): defer provider_created until host identity is applied

telemetry.provider_created was captured synchronously inside the core
factory, before VscodeTelemetryPolicyService resolves getHostVersion —
so that one event always carried the construction-time fallback identity
(pre-existing: it reported the hardcoded VSCode identity on JetBrains
and never had host_plugin_version).

Add an opt-in deferProviderCreatedEvent to the core telemetry factories
that skips the construction-time capture and exposes it as
ConfiguredTelemetryHandle.emitProviderCreated; the policy service emits
it right after applying the resolved host metadata. Other handle
consumers (CLI, hub daemon, examples) keep immediate emission.

Also close the subscription race on the same guarantee: a host setting
flip arriving while getHostVersion is still resolving now waits for the
metadata to be applied before opening the gate, and a slow initial
settings fetch no longer overwrites a newer subscription update.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): emit deferred provider_created on early dispose

If the policy service is disposed while the host-version lookup is
still pending, the deferred provider_created would never be captured —
the undeferred event was always emitted (with construction-time
identity) and exported by the shutdown flush. Emit-once semantics:
dispose fires the event with the fallback identity before shutting the
handle down, and the late metadata continuation cannot double-emit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:25:53 +09:00
Bee ae033761ab feat: auto generate built-in provider list (#12204)
* feat: auto generate built-in provider list

- Generate `providers.generated.ts` and `provider-ids.generated.ts` from `models.dev/api.json` alongside the model catalog
- Merge generated provider specs with handwritten built-in overrides for Cline, Codex, local/OAuth providers, routing metadata, and product defaults
- Include additional `models.dev` providers only when they are OpenAI-compatible for now
- Keep lightweight provider ID utilities from importing the full generated provider spec catalog

* Removed redundant handwritten definitions for providers that are fully described by generated metadata

* update unit test
2026-07-23 14:27:59 +02:00
Mikołaj Kondratek c961ae7730 feat(telemetry): emit host_plugin_version metadata on all events (#12478)
* feat(telemetry): emit host_plugin_version metadata on all events

The host already reports its Cline distribution version over the
hostbridge (getHostVersion.clineVersion — the JetBrains plugin version
on JetBrains, the extension version on VSCode), but telemetry never
attached it: extension_version is always the cline-core bundle version,
so JetBrains events could not be tied to a plugin release.

Attach it as a new optional host_plugin_version metadata field, omitted
when the host does not report one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(telemetry): loop over host version cases instead of interleaving stubs

Review feedback: the two host_plugin_version cases were interleaved via
onFirstCall/onSecondCall stubs across two service instances. Run one
mock-assert-reset cycle per case so the only differences between them —
the host version response and the expected reported value — are visible
in the case table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(telemetry): guarantee stub cleanup in host_plugin_version test

Review feedback: the loop installed process-global stubs and only
restored them on the happy path — a rejected create() or failed
assertion would leak exhausted stubs into subsequent tests and leave
the service undisposed. Use a sinon sandbox restored in finally, and
dispose the service there too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(telemetry): carry host_plugin_version on SDK-pipeline events too

Review feedback: main's production controller emits task lifecycle,
token usage, tool usage, and provider-failure events through a separate
SDK telemetry service whose metadata is built independently, so those
events still omitted the plugin version.

Add the optional host_plugin_version field to the shared SDK
TelemetryMetadata contract and resolve it from the authoritative
getHostVersion response during the policy service's init. The metadata
update is sequenced before the host telemetry setting is applied, and
events stay gated until that setting lands, so no event can be emitted
without the field in place. A failed host-version lookup degrades to
the previous behavior (field absent, telemetry still enabled).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 18:42:37 +09:00
Dominic Cooney e940b6a335 fix(vscode): preserve edit preview focus (#12491)
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-07-23 16:23:18 +09:00
David Knaack 5a0780a6f9 fix(llms): set metering header and use fetch adapter for sap ai core (#12337) 2026-07-23 00:05:27 -07:00
Bee 847276f4a5 feat(desktop): add one-time routines and run navigation (#12477)
* feat(schedules): add one-time routines and run navigation

* fix schedule review concerns

* fix repository lint errors

* fix optional auth request ID telemetry

* fix one-time schedule lifecycle

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-22 16:56:49 -07:00
Dominic Cooney 8b4d1973b5 fix(vscode): reclaim unobserved fallback terminals (#12352)
Classify unobservable terminal outcomes so cleanup and reporting share one source of truth. Reclaim managed sendText fallbacks at the disclosed next-acquisition boundary while preserving markerless, continued, detached, and uncertain-error terminals. Make cleanup, CWD reservations, process listeners, and detached logs failure-safe.
2026-07-22 16:22:11 -07:00
Dominic Cooney a5211a3a5c fix(vscode): make 'Proceed While Running' sticky to the command batch (#12359)
Register every foreground command before terminal acquisition so a parallel batch observes one Proceed While Running decision. If a command is still acquiring a terminal, settle its tool result immediately and transfer the approved command to an owned detached lifecycle that logs acquisition, output, completion, and failure. Abort before startup unregisters the handle and prevents the command from starting later.
2026-07-23 06:39:38 +09:00
Saoud Rizwan fb1333fdc2 fix(desktop): route external link opens through sidecar so they work in Tauri (#12481)
* fix(desktop): route external link opens through sidecar so they work in Tauri

The markdown 'Open external link?' dialog confirmed via window.open, which
the Tauri webview silently drops (no window opener configured), so clicking
'Open link' did nothing (ENG-2302). Route confirmation through
openExternalUrl, which invokes the open_external_url sidecar command inside
the Tauri shell and falls back to window.open in plain web mode.

Also fixes the marketplace 'Get value' env-var link, which relied on the
same dead target=_blank behavior.

* fix(desktop): open mailto/tel links and middle-clicked marketplace links

Greptile review fixes:
- open_external_url now allows mailto: and tel: alongside http(s) — the
  platform openers already dispatch any scheme to the OS protocol handler,
  the gate is just the allowlist. Streamdown's harden step blocks every
  other scheme before it reaches SafeMarkdownLink (test added to guard
  that assumption, since the sidecar allowlist relies on it).
- Protocol-relative URLs pass streamdown but fail the sidecar's new URL()
  parse; pin them to https before handing off.
- The marketplace 'Get value' link now intercepts middle clicks (auxclick)
  too, which bypassed the onClick handler and fell into the dead
  target=_blank path.
2026-07-22 14:27:01 -07:00
Renee Huang 59113c309c docs: update ClinePass wording to '2-5x the usage on popular open coding models compared to standard API rate' (#12479)
* docs: update ClinePass wording from '2-5x API rate limits' to '2-5x the usage on popular open coding models compared to standard API rate'

* docs: update ClinePass wording in cline-provider.mdx for consistency

* nit
2026-07-22 11:44:55 -07:00
TheRealSpencer 48d0c38f52 fix(security): bump axios to 1.18.0 to resolve multiple CVEs in axios (#12473)
* fix(security): bump axios to 1.18.0 to resolve multiple CVEs in axios

* fix(security): bump axios to 1.18.0 in docs project
2026-07-22 12:38:45 -05:00
aikido-autofix[bot] c7ab9ff839 fix(security): update js-yaml from 4.1.1 to 4.3.0 (#12456)
Co-authored-by: aikido-autofix[bot] <119856028+aikido-autofix[bot]@users.noreply.github.com>
2026-07-22 09:49:42 -05:00
Tomás Barreiro 045518d19f Add Request-ID to auth events (#12444)
* Add the X-Request-ID to the ClineOAuthTokenError

* Add request id to events

* address comments
2026-07-22 13:20:30 +09:00
Saoud Rizwan 099c6179e4 fix(desktop): resolve login shell PATH so agent can find gh and other CLI tools (#12429)
* fix(desktop): resolve login shell PATH so agent can find gh and other CLI tools

When the Tauri app is launched from Finder/the Dock on macOS it inherits
launchd's minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin), so the sidecar and
every process it spawns for the agent (bash tool, MCP servers) can't find
tools installed via shell profiles, e.g. Homebrew's gh in /opt/homebrew/bin.
The same task works from the CLI because a terminal runs with the full
login-shell PATH.

At sidecar startup, ask the user's login+interactive shell for its PATH
(sentinel markers isolate it from profile noise, 5s timeout, kill on hang)
and merge it into process.env.PATH: shell entries first, current-only
entries preserved. No-op on Windows; CLINE_SIDECAR_SKIP_SHELL_PATH=1 is the
escape hatch. Failures never block startup.

Fixes CLINE-2740

* fix(desktop): address greptile review on shell PATH resolution

- Don't let shell resolution eat the Tauri endpoint-readiness window: kick
  it off first so it overlaps sidecar startup (awaited before the session
  manager exists, which is what spawns children), drop the shell timeout
  5s -> 2s, and give the fallback attempt half the budget so the combined
  worst case (3s) stays inside the 5s readiness poll.
- Handle non-POSIX login shells: run the marker printf inside /bin/sh so
  $PATH expansion never depends on the outer shell's rules (fish would
  space-join it), pass -i/-l/-c as separate flags, give csh/tcsh only -c
  (their -l is valid only as the sole flag), and retry with the platform
  default shell when $SHELL can't produce a PATH.
- Don't log the resolved PATH: the applied result now carries an entry
  count instead of the merged PATH string.

* fix(desktop): read login shell from the account database, document PATH resolution

$SHELL is set by a parent shell, so a GUI-launched process may not have it.
Use os.userInfo().shell (getpwuid — DirectoryServices on macOS, same source
as dscl UserShell; NSS/etc/passwd on Linux) as the authoritative source,
with $SHELL and the platform default as fallbacks. Also documents the whole
mechanism in the app README.

* fix(desktop): widen endpoint readiness poll, source csh login profile

- The 5s get_desktop_backend_endpoint poll was already tight for
  session-manager init on slow machines; shell PATH resolution (bounded 3s
  worst case) made it tighter. Poll 15s instead — it returns as soon as the
  ready line arrives, so only genuine failure waits longer.
- csh/tcsh can't take -l alongside -c, so mark them as login shells via the
  argv[0] dash convention (argv0: "-tcsh") to get ~/.login sourced on top
  of the always-read rc file.

* fix(desktop): never spawn a second sidecar while one is alive

ensure_desktop_backend_started treated a live child with a pending
endpoint as absent and fell through to spawn a duplicate, orphaning the
first process. Hold the process lock across the whole check-and-spawn
(concurrent callers serialize), return early for any live child, fail
the endpoint poll fast when the child exits instead of respawning, and
stop a stale stdout-reader from wiping a successor's endpoint. The spawn
is injectable so regression tests cover repeated and concurrent startup
checks (exactly one spawn while pending) and dead-child replacement.

* style(desktop): tighten mergePaths and csh comment per review

* docs(desktop): codify backend state lock ordering
2026-07-21 18:11:55 -07:00
Bee 26037b17ac feat(core): default to agentic compaction (#12317)
* feat(core): default to agentic compaction

Use agentic compaction when no valid strategy is configured while preserving explicit basic selection. Add a session compaction CLI and package script for testing and comparing compaction strategies.

* createHandlerMock

* fix(core): let the agentic compaction cut land on assistant boundaries

Agentic auto-compaction only accepted typed user messages (turn starts)
as cut boundaries. The canonical host transcript — one typed task
followed by a long assistant tool_use / user tool_result loop — has no
turn start past index 0, so findCutIndex snapped to 0 and
runAgenticCompaction returned undefined: the UI showed "auto-compacting"
then "auto-compaction-skipped" on every turn while the context kept
growing. Re-compaction had the same failure permanently, because the
projected transcript starts with a compaction summary message, which is
excluded from turn starts.

Assistant messages are equally safe boundaries: an assistant's tool_use
keeps its result in the user message that follows it, so a cut there
never orphans half of a tool pair. Typed-user protection is preserved —
when a typed turn exists past index 0 the cut still stays at or before
it, so the latest typed prompt is never folded into the summary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* add compaction fixtures for testing

* basic compaction improvement

* feat: attach metadata to the merged compaction message

* fix(core): address review comments on compact-session script

- add cline provider to the API key env defaults (CLINE_API_KEY)
- accept legacy string-content messages in readMessages
- print usage instead of a stack trace when --provider/--model are missing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(core): preserve basic compaction across restores

## Summary

- keep tool-result message IDs stable across restore/persist round-trips
- preserve concluding assistant responses as real messages during basic compaction
- freeze prior compaction output so later passes only fold newly added history
- accumulate removed-message and usage metadata across repeated compactions
- update the basic compaction fixture and regression coverage

## Problem

Tool-result IDs were re-suffixed every time persisted messages were converted
back into agent messages. Because compaction state hashes the source message
prefix, restoring a session changed that hash and invalidated an otherwise
successful compaction, causing the full transcript to be sent again.

Basic compaction also reprocessed its own output on subsequent passes. This
could stack duplicate system notices, discard assistant conclusions retained by
the previous pass, and replace cumulative compaction statistics with values
from only the latest pass.

## Solution

Only add tool-result suffixes when splitting a mixed message, leaving already
split and single-result message IDs unchanged. Mark non-user compaction
survivors as preserved, carry those messages through future passes verbatim,
and budget older turns' final assistant answers as first-class messages.
Compaction metadata now adds prior removed-message and usage totals to the work
performed by the current pass.

## Validation

- 66 focused codec and compaction tests pass
- @cline/core typecheck and smoke typecheck pass
- Biome checks pass for all changed TypeScript files
- git diff --check passes

* fix unit test

* fix compaction defaults and fallback

* fix basic compaction credential lookup

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 17:41:58 -07:00
Saoud Rizwan 1adce5e56d chore(desktop): release v0.0.3 2026-07-21 17:31:29 -07:00
Saoud Rizwan 73583ea178 polish(ui): reasoning trigger reads just 'Thinking' — drop status text and brain icon (#12460)
The collapsed reasoning block header showed 'Thought process · Complete'
with a brain icon; PM feedback is that the status text and icon read as
noise. The trigger is now just the label + disclosure chevron, with
'Thinking' as the default label in both streaming and complete states.
Removes the now-unused cline-chat-reasoning-status style and BrainIcon.
2026-07-21 17:28:25 -07:00
Saoud Rizwan 21a0141b86 chore(desktop): release v0.0.2 2026-07-21 17:00:20 -07:00
Saoud Rizwan ecb71ba7cc fix(desktop): make scheduler row actions work and add hover tooltips (#12428)
* fix(desktop): make scheduler row actions work and add tooltips (CLINE-2745)

- Replace the view icon's window.alert (a no-op inside the Tauri webview)
  with a proper schedule-details dialog
- Trigger schedule.trigger with wait: false so 'run now' queues the run
  and returns immediately instead of blocking until the whole agent run
  finishes (which outlived the webview's 120s request timeout)
- Add hover tooltips to all schedule row actions (view, edit, run now,
  pause/resume, delete, enable switch)
- Show a spinner on the run-now button while triggering and toast on
  success/failure
- Return lastExecutions from list_routine_schedules so 'Last result'
  actually populates (it was always '-')
- Mount <Toaster /> in the root layout; toast() calls app-wide were
  previously rendered nowhere

* fix(desktop): per-schedule last executions and concurrent row actions

- list_routine_schedules backfills the latest execution for schedules
  whose runs fell outside the 50-newest global window (skipping
  schedules that have never run), so every row can show a last result
- busy/triggering row state is now a set keyed by schedule id, so one
  action finishing no longer clears another row's in-flight spinner

* fix(desktop): reject same-row schedule actions synchronously

Two rapid clicks on the same row action could both fire before React
rendered the disabled state; the first completion then cleared the
shared busy id while the second request was still pending (and run-now
would enqueue two runs). Guard entry through a ref that mirrors
busyScheduleIds so the duplicate click is rejected before any request
is sent.
2026-07-21 16:41:05 -07:00
Saoud Rizwan f5224abdf5 feat(desktop): auto-updates + automated signed releases from GitHub Actions (#12420)
* feat(desktop): auto-update via Tauri updater with restart prompt

The Rust shell now checks the desktop-latest GitHub release feed on launch
and every 2 hours, downloads and stages updates in the background, and
exposes get_update_status/restart_to_apply_update commands. The webview
polls the status and shows a persistent toast with a one-click restart once
an update is staged; ignored updates apply on next launch. Updater
artifacts are only produced with the CI config overlay
(tauri.release.conf.json) so local packaging keeps working without the
updater signing key. Also mounts the previously-unmounted Toaster so
existing toast() calls render.

* ci(desktop): add desktop-publish release workflow and publish-desktop skill

desktop-publish.yml mirrors cli-publish: dispatch with a desktop-vX.Y.Z
tag + confirm gate, validates the tag against package.json and
tauri.conf.json, builds signed+notarized DMGs for aarch64 (native) and
x86_64 (cross-compiled sidecar via bun --target), generates the updater
manifest, publishes the versioned GitHub release, refreshes the rolling
desktop-latest auto-update feed, and posts to Slack. Adds the release
skill, changelog, and README docs for the required GitHub secrets.

* fix(desktop): address review — outlast sidecar shutdown window, dedupe update toast across remounts

stop() now polls for 7s before escalating to kill, past the sidecar's own
5s SHUTDOWN_TIMEOUT_MS graceful-shutdown budget, so clicking Restart now
(or quitting) during session persistence can't SIGKILL the sidecar
mid-write. notifiedVersion moves to module scope so a page remount doesn't
re-toast an update the user already dismissed.

* docs(desktop): move publish-desktop skill to .cline/skills, slim README release section

Match the publish-cli convention: the skill lives in .cline/skills/ and is
symlinked from both .agents/skills/ and .claude/skills/ so all agents pick
it up. The README's release section shrinks to a pointer + the two
never-lose invariants (desktop-latest feed, updater private key); the repo
secrets table moves into the skill, which also fixes its dangling reference
to a 'Release automation' README section and escapes the pipe that broke
the GFM table cell.
2026-07-21 16:37:18 -07:00
Saoud Rizwan 85484abf7a feat(desktop): align settings page with hub dashboard (#12427)
* feat(desktop): align settings with hub dashboard (ENG-2286)

- Break out Customizations into its own sidebar nav group (Plugins,
  Skills, MCP, Hooks, Rules, Agents, Tools), mirroring the hub
  dashboard's customizations break-out, replacing the single
  Customizations entry (Rules-only) and the MCP Marketplace entry
- Port the hub's account view: signed-out state with working Sign
  in/Sign out (the old Sign Out button had no handler), auth-error
  detection, disabled tabs when signed out, PageFrame/PageHeader layout
- Port the hub's add-provider view for consistent PageFrame layout

* fix(desktop): merge the two MCP sidebar entries into one

The sidebar showed MCP twice: 'MCP Servers' under Settings (full
management: add/edit/toggle/delete) and 'MCP' under Customizations
(marketplace browse with uninstall-only cards). Keep the single 'MCP'
entry under Customizations to match the hub sidebar, and route it to
McpServersContent with the marketplace embedded: the management cards
now render as the marketplace view's Installed section, so one page
covers add/edit/toggle/delete plus catalog install.

* fix(desktop): stop long marketplace taglines forcing page-wide overflow

line-clamp (webkit-box) paragraphs report their full unwrapped text
width as intrinsic min-content, and grid/flex items default to
min-width:auto, so long MCP taglines pushed the whole marketplace grid
(and the page) wider than the viewport. Add min-w-0 at each grid-item
level so cards clamp to the container and the tag row scrolls within
itself.
2026-07-21 16:33:54 -07:00
Saoud Rizwan 0b0e2fbab8 feat(desktop): add open-in-editor and copy-path actions to diff view (#12434)
* feat(desktop): add open-in-editor and copy-path actions to diff view

Adds per-file actions to the session diff view (CLINE-2738):
- copy the file path (resolved to an absolute path against the session cwd)
- open the file in a code editor via a new open_file_in_editor sidecar
  command that prefers editor CLIs (code/cursor/windsurf/zed/subl) and
  falls back to macOS app bundles, then the OS default opener

* fix(desktop): handle Windows editor shims and mount Toaster for failure feedback

Address greptile review on #12434:
- route .cmd/.bat editor shims through cmd.exe (spawn can't launch them
  directly) and attach spawn error listeners so async launch failures
  fall back to the OS opener instead of crashing the sidecar
- mount the app-wide Toaster (same lines as #12428) so copy/open failure
  toasts are actually visible

* fix(desktop): guard Windows shell launches against cmd metacharacters

cmd.exe re-parses metacharacters inside arguments even when Node quotes
them (the reason spawning .cmd files without a shell is banned), so a
file path like 'report & evil.cmd' handed to the cmd /c shim launch
could execute a second command. Reject such paths with a clear error on
win32 and skip shim executables containing metacharacters (CodeQL
js/shell-command-injection-from-environment on #12434).

* feat(desktop): editor picker dropdown + copy button next to path in diff view

Review feedback on #12434:
- Renee: open-in-editor is now a dropdown listing the editors actually
  installed on the machine (new list_available_editors sidecar command;
  PATH CLIs + macOS app bundles), plus a system-default entry.
  open_file_in_editor accepts an optional editor id; omitted keeps the
  old auto-cascade, so older sidecars and existing callers still work.
- Beatrix: copy-path button now sits right after the filename (GitHub
  style) instead of grouped at the right edge; an invisible flex spacer
  keeps the dead space clickable as a collapse toggle.

* feat(desktop): brand icons + kanban editor set in diff-view editor picker

Match the kanban open-in dropdown: monochrome brand glyphs (VS Code,
Cursor, Windsurf, Zed, Xcode, IntelliJ IDEA) rendered inline with
currentColor so they follow the theme, an 'Open in' menu header, and a
system-default entry with a generic icon. Catalog grows to the kanban
editor list (adds VS Code Insiders via code-insiders, IntelliJ via
idea, Xcode via xed; macApps is now a list so IntelliJ CE is found).
Sublime Text keeps a generic file-code glyph (kanban has no sublime
icon).
2026-07-21 16:31:48 -07:00
Saoud Rizwan 1e2e8fe81b fix(desktop): preserve MCP server oauth tokens and metadata across dialog edits (#12426)
* fix(desktop): preserve oauth and metadata when upserting MCP servers

upsert_mcp_server rebuilt the settings record from scratch, so editing a
remote server through the dialog silently wiped its oauth block (tokens)
and any plugin-ownership metadata. Merge machine-managed fields from the
existing record (following previousName across renames) into the upserted
entry.

* fix(desktop): drop MCP server oauth tokens when transport or URL changes

Editing a remote server's URL or transport previously carried the old
server's OAuth tokens onto the new registration, sending credentials
issued for one endpoint to a different one. Preserve oauth only when
the effective transport type + URL are unchanged (rename-safe).

* fix(desktop): treat legacy "http" MCP transport as streamableHttp alias

Core config-loader maps transportType "http" to streamableHttp, so a
legacy record resaved through the dialog is the same endpoint; without
normalizing, mcpTransportIdentity saw it as changed and dropped oauth.

* fix(desktop): default typeless URL-based legacy MCP records to sse

Core config-loader resolves a legacy flat record with a url but no
type/transportType as sse, while the sidecar defaulted to stdio. That
skewed mcpTransportIdentity (dropping oauth on a no-op edit) and made
list_mcp_servers report such records as stdio to the dialog.
2026-07-21 16:24:28 -07:00
Tomás Barreiro 78c6724c6a Add auth metadata to the auth telemetry (#12274)
* Add session and user id to auth telemetry events

* Add the auth metadata

* Address comments

* Add metadata to successful events

* remove user ids from the types

* fix tests

* address comments

* replace startedAtMs with sessionDurationMs

* fix tests

* update based on latest main

* fix imports
2026-07-22 00:41:43 +02:00
Bee f2a895cf86 fix(desktop): rebuild sessions when switching providers (#12454)
* fix(desktop): rebuild sessions when switching providers

Recreate active sessions with their existing transcript and compaction state before sending to a different provider. Preserve provider-specific connection settings and distinguish provider changes from model-only updates.

Add coverage to verify provider switches rebuild the session before sending.

Currently SendSessionInput has no provider/model configuration, so the desktop client must perform that lifecycle transition before sending. The cleaner long-term API would make provider selection part of an atomic turn request—something like send({ sessionId, prompt, providerId, modelId })—and let Core decide whether rebootstrap is necessary.

* fix(desktop): harden provider session transitions

* fix(desktop): make provider rebuilds transactional
2026-07-21 15:20:15 -07:00
Saoud Rizwan 1585999251 fix(desktop): make account page functional (#12424)
* fix(desktop): make account page functional

The account page rendered data but every interaction was dead:

- Sign Out button had no click handler at all. Wire it to clear the
  cline provider auth (same flow as cline-hub), show a signed-out card
  with a working Sign In (browser OAuth) instead of a raw error + Retry,
  and refresh the shared account context so the sidebar identity updates.
- Organization rows were static divs. Make them switchable (including a
  Personal row) via the existing cline_account switchAccount operation,
  with a pending spinner and overview + context reload after switching.
- External links (+ Credit, + Create org, open dashboard) used
  target=_blank anchors, which are silently dropped inside the Tauri
  shell (no window opener configured). Route them through a new
  open_external_url sidecar command that opens the host default browser
  (http/https only); plain web mode falls back to window.open.
- + Credit pointed at the organization credits page even for personal
  accounts; use dashboard/account?tab=credits when no org is active.
- Guard the browser-open spawn with an error listener so a missing
  opener binary can't crash the sidecar with an unhandled error event.
- Disable Usage/Billing tabs while signed out (they can only error).

Closes CLINE-2737

* fix(desktop): harden external URL opener and auth error classification

- open URLs on Windows via rundll32 instead of cmd /c start so URL
  metacharacters cannot be parsed as shell operators
- surface opener spawn failures instead of always reporting opened: true
- classify only definitive signals (missing token, re-auth required,
  status 401) as signed-out; transient refresh/permission errors keep
  the retryable error UI

* fix(desktop): reject external URL open when the launcher exits non-zero

The opener promise resolved on the spawn event, so a launcher that
started but failed to hand off (xdg-open exits 3 when no handler is
available) still reported opened: true. Reject on a fast non-zero exit;
if the launcher is still running after a 2s grace window, assume the
handoff worked rather than blocking on a launcher that lingers.
rundll32 exits 0 even on failure, so Windows stays best-effort.
2026-07-21 15:19:02 -07:00
Saoud Rizwan 2c556a4c94 feat(desktop): simplify Add MCP Server dialog with Local/Remote server types (#12425)
Replaces the raw stdio/sse/streamableHttp transport dropdown with a
plain-language Local vs Remote choice (CLINE-2748). Local (stdio) stays
the default per the MCP spec's "Clients SHOULD support stdio whenever
possible"; picking Remote defaults to Streamable HTTP with SSE offered
as a legacy option. Working directory and Metadata JSON move behind an
Advanced collapsible (auto-expanded when editing a server that uses
them), and the server list badge now shows friendly transport labels.
2026-07-21 14:53:33 -07:00
Saoud Rizwan 9a80fa04c2 fix(desktop): keep thinking indicator visible until first model output (#12432)
* fix(desktop): keep thinking indicator visible until first model output

The webview only rendered the Thinking indicator while the chat status
was 'starting', but Core reports 'running' as soon as the turn is
dispatched -- well before the first streamed token arrives. The spinner
flashed for the RPC roundtrip and then disappeared, leaving ~1s of dead
air (model time-to-first-token) before the assistant bubble appeared.

Keep the indicator up while the session is running and the model has
not produced output yet: no streaming assistant message, last visible
message is the user's prompt, and no approvals/questions pending.

Closes CLINE-2739

* test(desktop): tighten thinking indicator test formatting
2026-07-21 14:52:21 -07:00
Saoud Rizwan 22a1fa2c84 feat(cli): upgrade opentui 0.1.102 -> 0.4.3 (#12453)
* feat(cli): upgrade opentui 0.1.102 -> 0.4.3

Brings the TUI stack up from April's 0.1.102 to the current 0.4.x line
(0.4.4/0.4.5 are <7 days old and blocked by the registry release-age
gate; bump again once they age out).

- @opentui/core + @opentui/react 0.1.102 -> 0.4.3
- opentui-spinner ^0.0.6 -> ^0.0.7 (0.0.7 peers on @opentui/core ^0.3.4)
- react-reconciler pin 0.32.0 -> 0.33.0 to match @opentui/react 0.4.x

@opentui-ui/dialog stays at 0.1.2 (abandoned upstream, peers ^0.1.69 so
bun warns on install) but its runtime surface (DialogProvider,
useDialog, useDialogKeyboard) works against core 0.4.3 - the tui-test
command-palette spec renders a real dialog in a pty and passes.

Validation: tsc clean, unit 889/890 (the one failure repros on an
untouched main checkout - stale bun pm pack guard expectation), tui-test
62/62 across repeated runs.

* fix(cli): force single opentui generation via root overrides

The previous commit left @opentui-ui/dialog's ^0.1.69 peer range
unsatisfied by core/react 0.4.3, so bun recorded nested
@opentui/core@0.1.102 + @opentui/react@0.1.102 copies under the dialog
package in bun.lock. Local installs happened to link the dialog against
the hoisted 0.4.3 store variant (which is why tui-test passed), but a
fresh install from the lockfile - CI, release builds - would follow the
nested entries and run two renderer generations in one process: dialog
components extending 0.1.102 Renderable classes inside a 0.4.3 renderer
tree.

Pinning @opentui/core and @opentui/react in the root overrides block
forces every consumer, dialog included, onto 0.4.3. The nested lockfile
entries are gone and a runtime identity check confirms
DialogContainerRenderable's prototype chain reaches the same class
objects as the 0.4.3 core the app imports.

Side effect: changing overrides makes bun fully re-resolve the
lockfile. The only drift is ~108 @radix-ui entries nested under the
vscode webview-ui workspace moving to newer patch versions (~1.1.15 ->
~1.1.19); webview-ui's full build (tsc -b && vite build) passes with
them. This drift would land at the next release anyway since bun run
version deletes and re-resolves bun.lock.

Re-validated: tsc clean, tui-test 62/62, unit 889/890 (same single
pre-existing bun pm pack guard failure that repros on untouched main).
2026-07-21 14:49:26 -07:00
Parafee41 57d364ffc2 fix(cli): keep status delivery failures non-fatal (#12401)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-21 14:18:02 -07:00
TheRealSpencer 0912f34286 fix(deps): bump mermaid to 11.16.0 and protobufjs to 7.6.5 (#12445)
Address mermaid CVEs (CVE-2026-41148/41149/41150/41159) and
protobufjs CVEs (CVE-2026-54269, CVE-2026-48712) by pinning
patched versions via package deps and workspace overrides.
2026-07-21 15:38:59 -05:00
Etisha Garg bdb216c110 Revert "docs: add Kimi K3 to ClinePass documentation (#12380)" (#12407)
This reverts commit cc29955c2d.
2026-07-21 10:02:58 -07:00
Mikołaj Kondratek c92d4e7553 fix(sdk): preserve file line endings in the editor tool executor (#12305)
* fix(sdk): preserve file line endings in editor tool executor

The native editor executor split and joined file content on "\n" only.
On CRLF files (common on Windows), insertInFile left existing lines with
trailing "\r" while inserted lines were LF-only, producing mixed line
endings. Because reads go through readline with crlfDelay (which strips
"\r"), the model always emits LF-only old_text, so subsequent exact-match
replaceInFile calls failed; multi-line replace on pure-CRLF files was
broken the same way.

Detect the file's dominant EOL and normalize: insertInFile now splits
content and new_text on /\r\n|\n/ and joins with the detected EOL, and
replaceInFile normalizes old_text/new_text to the file's EOL before
matching. The str_replace diff output also splits on /\r\n|\n/ so it no
longer embeds stray "\r" in diff lines sent back to the model.

Reported via JetBrains marketplace review #141234 (DeepSeek + CLion on
Windows).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sdk): address review — accurate EOL doc, literal $-sequences in replace

Reword the detectLineEnding JSDoc: it is a presence check for CRLF, not a
majority vote, so say so instead of claiming "dominant" EOL.

Use a replacer function in replaceInFile so "$"-sequences in new_text
($&, $', $`, $$, $n) are inserted literally instead of being expanded by
String.prototype.replace. Pre-existing bug surfaced during review; adds a
regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(sdk): clarify why EOL detection is a CRLF presence check

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 16:33:37 +09:00
Renee Huang b919a7e86c docs: mark .clineignore as deprecated soon (#12410)
* docs: mark .clineignore as deprecated soon

Add a deprecation notice to the .clineignore page and update pages that recommend it. Enforcement of ignore rules is extremely difficult (agents can get around them via @ mentions or shell commands), and the feature is orphaned in the VS Code/JetBrains extension (ClineIgnoreController), not part of the Cline SDK or CLI.

* docs: update clineignore deprecation wording

* wording changes

* update clineignore docs with plugin reference

* fix plugin example url

* edit clineignore docs file

* update formatting for clineignore doc

* clean up clineignore docs file

---------

Co-authored-by: Cline <bot@cline.bot>
Co-authored-by: TheRealSpencer <spencer@cline.bot>
2026-07-20 18:01:37 -07:00
Bee eefbe9fb18 feat(desktop): log telemetry events in desktop app (#12416)
* feat(desktop): log telemetry events in desktop app

* Address feedback
2026-07-21 02:50:56 +02:00
Bee 402b9994d8 feat(desktop): tool use group (#12415)
* feat(desktop): tool use group

* focus block

* apple p2 feedback
2026-07-20 15:24:05 -07:00
Bee fce1b97512 feat(desktop): improve session overviews and clarify workspace errors (#12414)
* feat(desktop): improve session overview and workspace errors

* address p1
2026-07-20 14:41:30 -07:00
Bee e7b0cec8e1 fix(desktop): diff view layout with proper scrolling (#12411)
Use full-height flex sizing, prevent header shrinking, and allow the scroll area to contract so file diffs remain scrollable within the view.
2026-07-20 23:04:53 +02:00
Bee 353ddc10f4 feat(desktop): add account context and window title utilities (#12348)
* fix(desktop): filter project paths

Best effort to remove desktop and user's home directory from showing up in project list in the desktop app.

* feat(desktop-app): add account context and window title utilities

- Add AccountContext provider and hooks for managing Cline account identity
- Add account-context.tsx and account-context.test.tsx
- Add desktop-window-title.ts and desktop-window-title.test.ts
- Update workspace-paths.ts with new utility functions
- Update agent-sidebar.tsx and agent-sidebar.test.tsx to use account context
- Update page.tsx to integrate account context
- Update sidecar/commands.ts to support account operations
- Update core SDK exports

This adds proper account identity management and window title utilities for the desktop app.

* dedup normalizeWorkspacePath

* home page update
2026-07-20 21:40:39 +02:00
Ara cabeb61036 Add minimal task lifecycle telemetry (#11851)
* Add minimal task lifecycle telemetry

* fix(vscode): use runner-safe auto approval assertions

* fix task lifecycle telemetry cancellation ordering
2026-07-20 20:45:02 +02:00
Etisha Garg cc29955c2d docs: add Kimi K3 to ClinePass documentation (#12380) 2026-07-20 09:23:02 -07:00
Saoud Rizwan c2faf38d72 chore(cli): release v3.0.46 2026-07-18 23:14:41 -07:00
Saoud Rizwan 4dab17769c fix(cli): detect real insufficient_credits error from Cline API (#12394) 2026-07-18 23:11:46 -07:00
Saoud Rizwan 396032cd3b chore(cli): release v3.0.45 2026-07-18 21:03:37 -07:00
Saoud Rizwan f33ab3a872 chore(sdk): release v0.0.65 2026-07-18 20:46:28 -07:00
Saoud Rizwan 2ca8364ffc docs: add Kimi K3 to ClinePass model list and reference pricing (#12393) 2026-07-18 20:35:48 -07:00
Saoud Rizwan 2ef81be703 feat(llms): make Claude Code and Codex provider packages optional peers (#12379)
ai-sdk-provider-claude-code and ai-sdk-provider-codex-cli were hard
dependencies of @cline/llms, so every npm install of the cline CLI
pulled their native binaries (~250MB claude-agent-sdk platform binary,
~105MB @openai/codex) even for users who never select those providers.

Move both to optional peerDependencies (kept as devDependencies so
monorepo builds still bundle the JS) and load them via literal dynamic
imports in community.ts, mirroring the existing opencode-sdk pattern.

The Claude Code provider now resolves the claude executable explicitly:
bundled platform package when present, otherwise a user-installed
claude from PATH, passed via defaultSettings.pathToClaudeCodeExecutable.
The agent SDK's own resolution cannot be used from Bun-compiled
binaries because it anchors on the virtual bunfs where node_modules
lookups never see packages on disk. Codex already degrades gracefully
(npx -y @openai/codex, then codex on PATH).
2026-07-18 20:30:24 -07:00
Saoud Rizwan 359445ae0c fix(sdk): stop exposing the team spawn tool to teammates (#12371)
* fix(sdk): retry runs once after refreshing expired OAuth credentials

Teammate and subagent sessions inherit the OAuth access token as a
snapshot at spawn time and had no refresh path: when the token expired
while the lead was blocked (e.g. in team_await_runs), their next model
call died with the provider's raw 401 body. Only the lead's turn-start
sync and runWithAuthRetry could refresh, and neither runs mid-turn.

Add an onAuthError hook to AgentConfig, wired once per session by
LocalRuntimeHost: it refreshes credentials through the shared
single-flight RuntimeOAuthTokenManager and propagates the new key to
the lead, delegated defaults, and all teammates via the existing
updateConnection channels. SessionRuntime retries a run once when it
failed with an auth-like error and the refresh succeeded, continuing
from the persisted trail so completed iterations aren't replayed.

Also fix isLikelyAuthError to lowercase string inputs; the server's
'Unauthorized: ...' message only matched when wrapped in an Error.

* fix(sdk): report errored teammate runs as failed instead of completed

Model-stream failures return results with finishReason 'error' rather
than throwing, so executeQueuedRun marked such runs 'completed' with
the error buried in resultSummary. Throw into the existing failure
path so the run reports status 'failed' (with run.error set and a
RunFailed event) and the retry machinery engages when maxRetries
allows.

* fix(sdk): stop exposing the team spawn tool to teammates

Spawning is lead-only, enforced at execution time, so teammates that
saw team_spawn_teammate in their toolset burned turns on 'Only the
lead agent can manage teammates.' rejections before falling back to
doing the work themselves.
2026-07-18 20:21:35 -07:00
Saoud Rizwan d9e2e9c76b fix(sdk): report errored teammate runs as failed instead of completed (#12370)
* fix(sdk): retry runs once after refreshing expired OAuth credentials

Teammate and subagent sessions inherit the OAuth access token as a
snapshot at spawn time and had no refresh path: when the token expired
while the lead was blocked (e.g. in team_await_runs), their next model
call died with the provider's raw 401 body. Only the lead's turn-start
sync and runWithAuthRetry could refresh, and neither runs mid-turn.

Add an onAuthError hook to AgentConfig, wired once per session by
LocalRuntimeHost: it refreshes credentials through the shared
single-flight RuntimeOAuthTokenManager and propagates the new key to
the lead, delegated defaults, and all teammates via the existing
updateConnection channels. SessionRuntime retries a run once when it
failed with an auth-like error and the refresh succeeded, continuing
from the persisted trail so completed iterations aren't replayed.

Also fix isLikelyAuthError to lowercase string inputs; the server's
'Unauthorized: ...' message only matched when wrapped in an Error.

* fix(sdk): report errored teammate runs as failed instead of completed

Model-stream failures return results with finishReason 'error' rather
than throwing, so executeQueuedRun marked such runs 'completed' with
the error buried in resultSummary. Throw into the existing failure
path so the run reports status 'failed' (with run.error set and a
RunFailed event) and the retry machinery engages when maxRetries
allows.
2026-07-18 20:19:17 -07:00
Saoud Rizwan d859a86a6f fix(sdk): retry runs once after refreshing expired OAuth credentials (#12369)
* fix(sdk): retry runs once after refreshing expired OAuth credentials

Teammate and subagent sessions inherit the OAuth access token as a
snapshot at spawn time and had no refresh path: when the token expired
while the lead was blocked (e.g. in team_await_runs), their next model
call died with the provider's raw 401 body. Only the lead's turn-start
sync and runWithAuthRetry could refresh, and neither runs mid-turn.

Add an onAuthError hook to AgentConfig, wired once per session by
LocalRuntimeHost: it refreshes credentials through the shared
single-flight RuntimeOAuthTokenManager and propagates the new key to
the lead, delegated defaults, and all teammates via the existing
updateConnection channels. SessionRuntime retries a run once when it
failed with an auth-like error and the refresh succeeded, continuing
from the persisted trail so completed iterations aren't replayed.

Also fix isLikelyAuthError to lowercase string inputs; the server's
'Unauthorized: ...' message only matched when wrapped in an Error.

* feat(telemetry): emit user.auth_run_retry when a run is retried after credential refresh

Addresses Greptile review on the auth-retry PR: the refresh itself was
already instrumented (auth_refresh_soft_failure / auth_logged_out fire
inside getValidClineCredentials), but the new retry transition was not.
The recovered flag counts runs that would previously have died with the
raw provider 401 — the direct production measure of this fix working.
2026-07-18 20:08:18 -07:00
Saoud Rizwan 0b7b9c1b3d fix(llms): add Kimi K3 to bundled ClinePass model fallback (#12392)
* fix(llms): add cline-pass/kimi-k3 to bundled model catalog fallback

* fix(llms): derive cline-pass default model from catalog authored order

Adding kimi-k3 (newest releaseDate) to the bundled cline-pass catalog
would have flipped firstGeneratedModelId — which sorts by release date —
to cline-pass/kimi-k3, silently changing the default model for new
ClinePass setups. Use the catalog's authored order instead, which mirrors
the recommended-models endpoint's curated order (intended default first,
subscription models before free ones).
2026-07-18 20:03:19 -07:00
Dominic Cooney 557d725690 fix(vscode): shell mismatch between prompt, execution, and user configuration on Windows (#12331)
* Rationalize shell identification and prompting, especially on Windows.

* Probe all pwsh install locations for the Windows default shell.

The default-shell fallback only checked the Program Files pwsh path,
so Microsoft Store installs of PowerShell 7 fell back to Windows
PowerShell while VS Code's own terminal launched pwsh. Share one
candidate list between the sync default-shell check and the async
PowerShell prober. Also drop an 'as string' cast that hid the
setting's type from the checker.

* Address shell resolution review feedback

* Resolve array-valued terminal profile paths on macOS and Linux too

VS Code permits terminal profile 'path' to be string | string[] on every
platform, not just Windows. The resolver (env expansion, first-existing
selection, PATH lookup) is now platform-generic: it uses the host path
module's separators and delimiter, probes PATHEXT only on Windows, and
treats env var names case-insensitively only on Windows. The macOS and
Linux getters route through it instead of returning the raw config value,
which crashed getShellKind() for array values.

* Apply terminal profile changes at the model-request boundary

A terminal profile change previously triggered a deferred session rebuild
to refresh the run_commands tool description. While a task was running the
rebuild waited, so the description could name one shell while commands
executed in another for the rest of the turn.

Instead of rebuilding, createShellTool now accepts a shell provider
function and re-derives the description each time the runtime reads it,
which happens exactly when a model request is built. The VS Code tool
snapshots {profileId, shell} in that provider; both execution paths (the
background spawn and the foreground terminal, via a new profile parameter
on getOrCreateTerminal) consume the snapshot. Commands produced by an
in-flight inference therefore run with the shell the model was told about,
and a mid-turn profile change takes effect when the tool results are sent
back: the next request names and uses the new shell.

The profile-change session rebuild path (handleTerminalProfileChanged) is
removed along with its deferred-rebuild window.

* Use the real createShellTool in the vitest @cline/core stub

The stub's hand-rolled createShellTool duplicated the 'shell must be a
string' invariant instead of exercising the code that enforces it
(getShellKind via description building), so the array-valued-profile
regression test proved only that the stub threw, not that the real tool
survives. Re-export the real implementation from SDK source — the same
pattern the stub already uses for the apply-patch and editor executors —
and assert on the actual generated descriptions, including that a profile
change is reflected at the next description read.

* Harden shell profile path resolution edge cases

- Warn and skip profile paths containing variable references beyond
  \ (e.g. \) instead of silently probing a
  literal path that can never exist; later candidates and the platform
  default still apply.
- Document that an overriding bash executor in createBuiltinTools bypasses
  the resolved canonical shell and must honor it to keep the run_commands
  description truthful.
2026-07-18 04:23:00 +02:00
Saoud Rizwan 7274d8badc feat(ui): add agent chat components, Storybook, and npm releases (#12374)
* feat(ui): add shared agent chat components and Storybook

* ci(ui): add standalone npm publishing

* docs(ui): keep release commands environment-neutral

* refactor(ui): simplify package validation

* refactor(ui): tighten package and release contracts

* docs(ui): remove duplicate install guidance

* ci(ui): make publishing workflow manual-only
2026-07-17 18:22:57 -07:00
Bee d1837366c0 chore(llms): update model catalog (#12366)
Update model catalog with bun run build:models
Version updated to 1784318695007
2026-07-17 13:28:59 -07:00
Saoud Rizwan c380daf4a3 docs(ui): add adoption primer (#12367) 2026-07-17 13:21:15 -07:00
Bee c564045d81 chore(cli): includes version numbers in hub status output (#12358)
Includes version numbers in hub status output and doctor command to make debugging with user easier.
2026-07-17 05:37:54 +02:00
Saoud Rizwan 9a5e1751b2 chore(cli): release v3.0.44 2026-07-16 18:38:54 -07:00
Saoud Rizwan 131e25e1a1 chore(sdk): release v0.0.64 2026-07-16 18:14:58 -07:00
Saoud Rizwan a7ff007af9 chore(cli): release v3.0.43 2026-07-16 17:52:46 -07:00
Bee ef27f45080 fix: max output token handling (#12031)
* fix: max output token handling

* shared

* max reasoning budgetTokens

* fix unit test

* fix: address review feedback on max output token handling

- OpenRouter effort branch sends only reasoning.effort (OpenRouter rejects
  effort combined with reasoning.max_tokens)
- OpenAI Responses forwards explicit caller maxTokens for API-key usage;
  ChatGPT OAuth and synthesized gateway defaults are still omitted
- Gateway lifts the synthesized default output cap above explicit Anthropic
  reasoning budgets so max_tokens > thinking.budget_tokens holds

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address second-round review feedback on max output token handling

- Replace the gateway-only requestedMaxTokens field with a defaultedMaxTokens
  flag set when the gateway synthesizes a cap, so explicit maxTokens from
  direct provider callers is forwarded by default (greptile P1)
- Check the parsed hostname instead of a URL substring when detecting the
  ChatGPT OAuth backend (CodeQL)
- Drop the empty else-if branch in toAiSdkMessages in favor of an explicit
  emptiedByDroppedReasoning condition (greptile P2; biome rejects the
  suggested bare continue)
- Dedupe isPositiveFiniteNumber by exporting it from gateway.ts (greptile P2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: extract isPositiveFiniteNumber into providers/utils.ts

Move the shared helper to its own module as suggested in review instead
of exporting it from gateway.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: remove unrelated VS Code changes

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-16 17:50:22 -07:00
Dominic Cooney e3c6d51072 fix: recognize frontmatter with a leading UTF-8 BOM (#12277)
* fix(vscode): recognize SKILL.md frontmatter with a leading UTF-8 BOM

SKILL.md files saved with a UTF-8 BOM (e.g. by Windows Notepad's 'UTF-8 with BOM' encoding) were silently skipped and not recognized as skills, because gray-matter/regex-style frontmatter parsers require '---' at byte offset 0 and never accounted for the leading BOM byte sequence Node's utf-8 decoder does not strip.

Fixes the shared parseYamlFrontmatter() helper (used by skills, rules, workflows, and remote skill entries in the VS Code extension) and every duplicated ad-hoc frontmatter regex across the SDK/CLI/hub/desktop-app/example-plugin code paths to strip a leading BOM before matching.

Adds regression tests exercising the exact reported scenario (BOM-prefixed SKILL.md silently missing name/description) in frontmatter.test.ts, skills.test.ts, skill-frontmatter-toggle.test.ts, user-instruction-config-loader.test.ts, and configured-agent-config.test.ts.

Fixes https://github.com/cline/cline/issues/12151

* refactor(shared): centralize UTF-8 BOM stripping

* refactor(shared): add UTF-8 file readers

* docs: guide UTF-8 configuration reads
2026-07-17 09:37:20 +09:00
Saoud Rizwan 48bac25548 chore(sdk): regenerate lockfile for v0.0.63 2026-07-16 17:13:44 -07:00
Saoud Rizwan 37f5f104f3 chore(sdk): release v0.0.63 2026-07-16 16:47:40 -07:00
Saoud Rizwan 3577b52404 feat(core): emit mistake-limit telemetry from the session runtime (#12355)
Moves the task.mistake_limit_reached capture (#12354) from the VS Code
SdkController wrapper into @cline/core so every host (CLI, VS Code,
hub daemon) emits it via its session telemetry service.

The MistakeTracker gains an onLimitTelemetry hook fired exactly once
per limit hit, before the limit decision is resolved — including when
no onConsecutiveMistakeLimitReached callback is configured (the
default-stop path, which the extension-side capture missed). The
orchestrator wires the hook to captureMistakeLimitReached using its
reserved telemetry field, reading sessionId/modelId/providerId at fire
time so mid-session connection updates are reflected.

The now-redundant extension wrapper and TelemetryService method are
removed to avoid double-counting in VS Code.
2026-07-16 16:39:50 -07:00
Max 1843bc8ed0 fix(vscode): persist selected account organization (#12345)
* fix(vscode): persist selected account organization

* fix(vscode): ignore stale organization responses

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-16 16:08:07 -07:00
Max fead00ec57 fix(vscode): avoid duplicate OpenAI provider settings (#12346)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-16 16:07:48 -07:00
Max 238107d21c fix(vscode): preview auto-approved apply patches (#12349)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-16 16:05:24 -07:00
Saoud Rizwan 2063a661bd feat(vscode): capture telemetry when consecutive mistake limit is reached (#12354) 2026-07-16 15:58:21 -07:00
Dominic Cooney ec02d5862e Fix debug harness: run under node, drop ws dependency. (#12319)
The harness rotted after the npm-to-bun migration: the 'ws' package it imported is no longer in the dependency tree, and Playwright's _electron.launch() times out under bun (the debugee Electron starts but Playwright never finishes attaching; the same launch attaches in under a second under node). Use the runtime's built-in WebSocket for the CDP client and document that the harness must be run with node.
2026-07-17 07:43:32 +09:00
Mikołaj Kondratek 8452084842 fix: auto-discover OS trust anchors in the CLI wrapper (#11498)
* fix: auto-discover OS trust anchors in the CLI wrapper

The 3.x CLI ships as a Bun-compiled binary. Bun does not read the OS
trust store unless NODE_USE_SYSTEM_CA is set, and even with the flag its
Windows enumeration covers only the `Root` store, not `CA`/Intermediate
(verified empirically across the CLINE-2353 Windows repro rounds). So a
corporate MITM root is not trusted out of the box and inference fails
with "unable to get local issuer certificate". The pre-3.0 (Node) CLI
had no app-level CA handling either; users only succeeded by setting
NODE_EXTRA_CA_CERTS manually. The reporter's ask: have it just work
without the env var.

This follows the CLINE-2353 SDK fetch-threading change. That made the
inference client honor a host-provided proxy/CA-aware fetch, but on the
CLI Bun's global fetch is already proxy-aware and a fetch function
cannot cross the hub-daemon process boundary, so the CLI's missing piece
is trust material, not the fetch. Env vars do inherit across spawns.

The npm `bin/cline` wrapper runs on Node (not Bun), so it can read the
full OS store via tls.getCACertificates("system") (Node >= 22, no flag
required) — including the Windows `CA` store Bun skips — and hand the
certs to the Bun child via NODE_EXTRA_CA_CERTS, which both runtimes
honor. This mirrors the JetBrains plugin's configureCertificates(),
replacing "harvest from the IDE trust store" with "harvest from the OS".

The merge logic lives in a dependency-free, injectable-module CommonJS
helper (bin/ca-certs.cjs) so it is unit-testable and ships verbatim in
the generated wrapper package (publish copies bin/ wholesale). A
user-set NODE_EXTRA_CA_CERTS is merged ahead of the system certs; a
self-reference to the managed bundle is detected to avoid re-appending
every launch; when no system certs are available the user's setting is
left untouched. Writes are atomic (temp + rename) and owner-only.

Adds ca-certs.test.ts (13 cases) covering harvest filtering, user-bundle
PEM/DER/missing handling, newline-separated merge, managed-path
self-reference, and the no-system-certs no-op.

* fix: harden CLI auto-CA harvesting (review follow-ups)

Follow-ups from the CLINE-2353 review of the CLI auto-CA wrapper.

- H1: a legacy NODE_EXTRA_CA_CERTS set to an OS-path-delimited list
  ("a.pem;b.pem", the CLINE-2324 footgun Node never split) was stat'd as
  one file, failed, and silently dropped the user's certs. readUserCerts
  now tries the whole value as one file first, then splits on the OS path
  delimiter and reads each existing PEM, merging them all.
- M1: skip the rewrite when the managed bundle is already current, instead
  of re-harvesting and rewriting on every launch (mirrors the JetBrains
  hash-and-skip). configureNodeExtraCaCerts now returns a typed outcome
  (unchanged | written | write-failed-reused | write-failed |
  no-system-certs) with cert counts.
- M2: tolerate rename-over-existing failures (Windows EPERM/EBUSY when a
  concurrent child holds the file open) by removing the target and
  retrying, then falling back to a previously-written bundle. Combined
  with M1 the steady state no longer rewrites at all.
- M3: the wrapper prints a one-line diagnostic under CLINE_DEBUG=1
  (cert counts + managed path, or a warning when no OS certs were found
  or the write failed). Runs once per startup.
- M4: corrected the now-stale CLI guidance in shared/net.ts (the CLI no
  longer requires users to set NODE_EXTRA_CA_CERTS manually).
- L1: documented the auto-trust behavior, the managed ~/.cline bundle,
  the merge-not-replace override semantics, and CLINE_DEBUG in the CLI
  README.
- L4: trimmed the helper's file header; DI is still injectable for tests.

ca-certs.test.ts grows to 20 cases: adds readUserCerts (single path,
delimited split, missing-segment skip, managed-bundle exclusion, empty),
the unchanged/second-run skip, and a write-failure outcome via an
fs that throws.

* fix: address CLI auto-CA review issues (temp cleanup, cert count, test)

- writeBundle now hoists the temp path so the outer catch removes a
  partially-written temp file (e.g. ENOSPC / ACL failure mid-write).
  Previously only the inner double-rename failure cleaned up, so repeated
  disk-full/permission failures left a stale .tmp per launch in ~/.cline.
  The inner Windows-rename fallback now lets its failure fall through to
  the single cleanup path instead of duplicating rmSync.
- userCertCount now counts individual certificates (via countCerts, which
  tallies BEGIN CERTIFICATE markers) rather than the number of PEM files,
  so a user bundle with N intermediates reports N and is comparable to
  systemCertCount. countCerts is exported for testing.
- Adds tests for the write-failed-reused branch (stale bundle reused when
  the rewrite fails but the old file is still readable) and for countCerts
  (one file holding two certs reports 2).

* fix: warn when the CLI wrapper's Node cannot read the OS trust store

tls.getCACertificates("system") needs Node >= 22.15; on older hosts the
auto-CA harvest silently did nothing, which is indistinguishable from a
broken corporate proxy. Distinguish the missing-API case as its own
outcome (api-unavailable) and print a non-debug warning when the user
has no NODE_EXTRA_CA_CERTS of their own. Found in round-5 Windows
validation (wrapper under Node 22.1.0).

* fix: copy only certificate blocks into the managed CA bundle

Combined cert+key PEMs (nginx/haproxy-style server.pem) passed the
old contains-a-certificate check, so a user NODE_EXTRA_CA_CERTS
pointing at one duplicated the private key into the managed bundle,
where it outlives rotation of the original and gets no permission
tightening on Windows. Extract complete BEGIN/END CERTIFICATE blocks
instead; files with none are treated as not PEM, and certificates-only
files pass through byte-identical so the unchanged-skip stays stable.
Raised in PR review.

* fix: show the old-Node trust warning once per Node version

The api-unavailable warning printed on every CLI invocation, turning
an actionable nudge into stderr noise for users pinned to an old Node.
Stamp the warning per Node version under the cline dir: it shows once,
re-arms when the Node version changes, and a bookkeeping failure never
suppresses the diagnostic. Raised in PR review.
2026-07-16 10:14:15 -07:00
Dominic Cooney a41129a5db fix(vscode): restore 'Proceed While Running' for foreground terminal commands (#12320)
* First cut of 'proceed while running' for foreground tasks.

* Address review: flush partial line on detach; cap log before write; freeze partial output at detach.

* fix(vscode): cap detached command log replay
2026-07-15 22:46:37 -07:00
Saoud Rizwan 1ea34be611 chore(cli): release v3.0.42 2026-07-15 20:03:31 -07:00
Saoud Rizwan e72bc3cd14 chore(sdk): release v0.0.62 2026-07-15 19:46:58 -07:00
Tomás Barreiro 9c907af826 Send the Feature Flag Event when rolling out (#12325)
* Send the Feature Flag Event when rolling out

* Update apps/vscode-rollout/scripts/smoke-loader.mjs

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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-15 19:39:37 -07:00
Saoud Rizwan e8d3d82522 fix(core): omit telemetry from hub tool contexts (#12326) 2026-07-15 19:33:12 -07:00
Max 7f9d2e96d9 fix(ollama): restore native API routing so context window and timeout settings work (#12286)
The 4.0.0 SDK migration routed Ollama through the generic OpenAI-compatible
vendor (/v1/chat/completions), which cannot express Ollama's options.num_ctx.
Every model loaded at Ollama's 4096-token server default, truncating Cline's
prompt and breaking most features (CLINE-2603, CLINE-2566, CLINE-2572).

- Add a native Ollama vendor backed by ai-sdk-ollama (wraps the official
  ollama client); num_ctx derives from the resolved gateway model's
  contextWindow at the adapter boundary, defaulting to 32768
- Persist the Model Context Window setting in providers.json via the
  pre-existing provider-neutral contextWindow field (legacy
  ollamaApiOptionsCtxNum state key kept as read fallback / write mirror),
  and surface it as the selected model's contextWindow so the chat
  indicator, compaction budgets, and num_ctx all agree
- Project ProviderConfig.maxInputTokens (where ProviderSettings.contextWindow
  lands) onto the selected gateway model in both gateway builders so
  CLI/Core hosts honor the configured value too
- Stop falling back to the bundled Ollama-Cloud catalog when /api/tags is
  empty; local-model-source providers keep the user's committed model
  instead of silently selecting a cloud model (nemotron)
- Wire Request Timeout (ms) with the legacy semantics (response must start
  within requestTimeoutMs || 30000; streaming never cut off mid-generation)
- Settings UI: gate the context-window field until provider config loads,
  skip unchanged writes, drop the custom prompt checkbox

Fixes CLINE-2603, CLINE-2566, CLINE-2572

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-15 17:55:29 -07:00
Saoud Rizwan d618f8073a fix(vscode-rollout): align bundle versions and harden combined publish workflows (#12321)
* fix(vscode-rollout): align bundle versions in the stable AB workflow

Found by Max in local testing: the union manifest's version (what the
Marketplace and auto-update see) is the stitch input, but each bundle's
About tab and telemetry extension_version read that bundle's OWN
package.json — so the stable combined VSIX reported three different
versions (dispatch input / main's 4.0.0 / legacy's 4.0.8) depending on
where you looked. The nightly channel doesn't have this problem
(nightlify.mjs stamps one version into everything); this gives the stable
channel the identity-preserving equivalent: scripts/set-version.mjs stamps
the dispatch version into each checkout after install, before its build.

Also fixes a latent ab-package bug while restructuring the steps: the
next-bundle build never ran build:sdk, so the @cline/* workspace deps had
no dist and esbuild would fail on a fresh CI checkout (the workflow has
never run end-to-end — the publish environment gate blocked pre-merge
dispatches). Split install/build:sdk/align/build into separate steps,
mirroring the nightly workflow.

* fix(vscode-rollout): assert bundle sub-manifest versions in identity guardrails

Greptile round on #12321: the stable guardrail didn't assert version at
all. Went one further than the suggestion — both workflows' guardrails now
also assert each bundle sub-manifest's version (and name, for nightly)
matches the expected version, which is the check that actually regression-
guards the set-version.mjs/nightlify.mjs stamping (About tab + telemetry
extension_version read the sub-manifests, not the union). Expected version
routed through env rather than interpolated into the script body. Adds the
conventional paired test for set-version.mjs.

* fix(vscode-rollout): don't fail the nightly run when the tag push is rejected

First real combined publish (run 29454994164) published to both registries
successfully but the run went red at the last step: the default
GITHUB_TOKEN cannot create a ref whose commit modifies workflow files, and
HEAD was the #12253 squash merge which rewrote this very workflow. There
is no workflows permission grantable to the token, so this recurs any
night HEAD touched .github/workflows. The tag is bookkeeping — mark the
step continue-on-error so a successful publish isn't reported as a
failure. (Today's missing tag was pushed manually.)
2026-07-15 17:39:44 -07:00
Saoud Rizwan eb21ba583c fix(ci): restrict nightly publishing to main (#12322) 2026-07-15 15:35:09 -07:00
Saoud Rizwan f29c25395c feat(vscode-rollout): A/B loader and packaging for staged SDK extension rollout (#12253)
* feat(vscode-rollout): A/B loader and packaging for staged SDK extension rollout

Ship one marketplace VSIX containing a tiny loader plus two complete
extension bundles: next/ (SDK-based apps/vscode from main) and legacy/
(the legacy-extension branch). The loader picks one bundle per window
from a PostHog-flag-driven, sticky, one-way cohort assignment, activates
it with a Proxy-scoped ExtensionContext so each bundle resolves its
resources from its own subdirectory, and falls back to legacy (with
partial-registration cleanup and version pinning) if the next bundle
crashes during activation.

Includes the union-manifest generator with per-cohort when-clause
gating, the VSIX stitcher, a node-level loader smoke test, and the
ext-vscode-ab-package workflow that builds both refs and packages
(optionally publishes) the combined VSIX.

* fix(vscode-rollout): address rollout review feedback

* feat(vscode-rollout): versioned kill-switch, user-setting override, launch-cadence telemetry

Review follow-ups from #12253:

- Kill-switch is now scoped by version instead of boolean: the PostHog flag's
  payload carries {"maxKilledVersion": "x.y.z"} and the loader demotes only
  combined VSIXes <= that version, so killing a broken release never blocks
  the release that fixes it. Arming with no payload still demotes everything,
  and the old boolean memento format is normalized on read.

- cline.rollout.bundleOverride user setting (auto | next | legacy) as a
  manual escape hatch editable straight from settings.json: beats flags and
  the kill-switch in both directions, applies on window reload, reported as
  'override' on the activation event. Injected into the union manifest by
  gen-manifest so neither bundle has to know about it.

- parseRolloutFlags hardens flag typing: only a literal boolean true promotes
  (multivariate variants, numbers, junk fail safe), kill payloads are parsed
  defensively from /decide's JSON-string encoding.

- Activation events now carry ms_since_last_activation so the real window-
  reload cadence bounds how fast the rollout percentage gets dialed up.

- Walkthrough manifest invariant relaxed from byte-equality to structural
  equality (ids/media/completionEvents): the branches already diverge on one
  MCP step description, and since walkthrough markdown at the VSIX root comes
  from next regardless, hard-failing on copy tweaks bricked the release
  pipeline while protecting nothing. Copy divergence now warns and ships
  next's text.

* feat(vscode-rollout): identity-aware namespace, authoritative activation telemetry, nightly indicator

- Derive the setting section and sdkBundle context key from the packaged
  manifest name (cline.* for stable claude-dev, cline-nightly.* for the
  nightly identity, whose packaging rewrites the whole ID namespace);
  gen-manifest derives the same prefix for gates and the injected
  bundleOverride setting.
- Call the activated bundle's reportRolloutActivation export (merged on
  both branches) with attempted/actual/fallback — the authoritative
  extension.rollout.bundle_activated event, attributed via the bundle's
  variant-built telemetry. On crash fallback the LEGACY bundle reports it.
- Rename the loader's direct PostHog event to
  extension.rollout.loader_decision: it collided byte-for-byte with the
  bundles' event name under a different schema. It keeps the loader-side
  metadata (override, launch cadence, loader_version, extension_name) and
  gains double_failure for the both-bundles-dead case.
- Fix duplicate activation events on crash fallback: the recursive legacy
  activation no longer emits a second, contradictory fallback:false event.
- Nightly-only status bar indicator (Cline: Next / Cline: Legacy) so
  dogfooders can see which bundle a window is running.
- Union diverged engines to the newer requirement instead of hard-failing:
  main's VS Code engine (^1.101.0) has legitimately moved ahead of
  legacy-extension's (^1.84.0), which bricked every combined build.
- Smoke scenarios for all of the above.

* feat(vscode-rollout): publish the nightly as the combined A/B VSIX

Convert ext-vscode-publish-nightly.yml (cron + dispatch) from the
standalone SDK build to the combined loader + next + legacy package,
published as saoudrizwan.cline-nightly at <major>.<minor>.<unix-seconds>:

- scripts/nightlify.mjs reproduces publish-nightly.mjs's identity mutation
  (claude-dev -> cline-nightly, "cline. -> "cline-nightly., displayName,
  activity bar title) with the version as an explicit argument so ONE
  version reaches both bundle manifests and the union manifest. Runs after
  dependency install and before each bundle build.
- Both bundle builds get CLINE_ROLLOUT_VARIANT (next/legacy) in the nightly
  AND stable workflows — without it the merged rollout telemetry
  (extension_variant common prop + the authoritative bundle_activated
  capture) silently no-ops.
- dry-run dispatch input builds and uploads the installable .vsix without
  publishing or tagging; publish/tag steps are additionally gated to main,
  so the PR branch can be dispatched for pre-merge verification.
- Identity guardrails before packaging: nightly workflow asserts
  cline-nightly, the stable ab-package workflow asserts claude-dev.
- The nightly tag now records the legacy bundle sha in its message.
- README: nightly channel section (identity mapping, the two telemetry
  events and their owners, dry-run verification), and a note that the
  PostHog flags govern nightly only until the stable combined VSIX ships.

The single-bundle publish-nightly.mjs path remains for manual
feature-branch pre-release publishes; CI no longer invokes it.

* chore(vscode-rollout): harden nightly workflow gating

- Restore a job-level branch allowlist on the publish job (main + the
  rehearsal branch). Advisory defense-in-depth: the enforced gate is the
  PublishNightly environment's deployment-branch policy in repo settings,
  which must list the same branches; a dispatched branch runs its own copy
  of this file.
- Route the legacy-ref dispatch input through env instead of interpolating
  it into the run script body (script-injection hygiene; dispatch already
  requires write access).

* add otel vars to rollout build (#12316)

- Extension will not emit otel metrics to otel without these vars, so
adding those into the slow-rollout build workflow

Co-authored-by: Max Paulus 🥪 <max@cline.bot>

* fix(vscode-rollout): pass OTel env to the nightly legacy bundle build

Legacy's esbuild inlines OTEL_* at build time and its standalone publish
workflow passes them, so the combined nightly's legacy bundle was being
built with the OTel logs/metrics pipeline dead. Companion to #12316,
which fixes the same gap in ext-vscode-ab-package.yml (both bundles
there).

* feat(vscode-rollout): make the rollout two-way, remove the kill-switch

The one-way cohort + versioned kill-switch existed to avoid demoting users
whose SDK-bundle tasks aren't listed by legacy and whose rotated creds may
need a re-login. Decision: those are acceptable, temporary UX costs on an
emergency-only path — not worth a second flag and permanent mechanism
complexity (payload parsing, version scoping, killed-up-to cache format).

Now there is ONE knob: each background refresh caches exactly what
ext-sdk-bundle-rollout says for the next window. Dialing the percentage
down demotes; 0% pulls everyone back to legacy on their next reload.
Fail-safe direction preserved: only a literal boolean true promotes —
variant strings / numbers / a deleted flag all resolve to legacy; malformed
/decide responses leave the cache untouched. Local crash pinning (next
threw -> pin this version to legacy on this machine) is unchanged and
independent of the flag.

Removes KILLSWITCH_FLAG/KILLSWITCH_STATE_KEY/isVersionKilled/
normalizeKilledUpTo/compareVersions/nextCachedBundle; parseRolloutFlags
becomes parseRolloutAssignment returning the bundle to cache. Smoke
scenarios replaced with two-way promote/demote coverage.

---------

Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-07-15 15:07:45 -07:00
Max 84c9b587a6 refactor(vscode): resolve model metadata host-side (#12130)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-07-15 13:37:02 -07:00
Saoud Rizwan 6dca234d8e chore(cli): release v3.0.41 2026-07-15 11:02:17 -07:00
Renee Huang 9217eacbbd fix: update broken ACP editor integrations redirect to CLI reference (#12312)
* fix: update broken ACP editor integrations redirect to point to CLI reference

* feat: add ACP Editor Integrations page under CLI section

- Create cli/acp-editor-integrations.mdx with ACP overview, supported editors, quick start, and usage guide
- Add page to CLI navigation group in docs.json
- Restore redirect from /cline-cli/acp-editor-integrations to /cli/acp-editor-integrations (page now exists)

* Revert "feat: add ACP Editor Integrations page under CLI section"

This reverts commit 2728b9c2ad.
2026-07-15 10:57:59 -07:00
Saoud Rizwan adbb42a99c chore(sdk): release v0.0.61 2026-07-15 10:29:47 -07:00
Saoud Rizwan 50d1578a7e feat(ui): add shared Cline theme package (#12285)
* feat(desktop): refresh navigation and visual foundation

* feat(desktop): align settings with Cline Hub

* feat(desktop): improve chat markdown rendering

* fix(desktop): resolve review feedback blockers

* fix(desktop): refine inline code sizing

* fix(desktop): preserve workspace choices during startup

* feat(ui): add shared Cline theme package

* fix(desktop): restore sidebar session sorting

* fix(desktop): harden session startup state

* fix(desktop): tighten settings navigation state

* fix(desktop): tighten markdown link handling

* fix(ui): harden theme contract validation

* test(desktop): cover late workspace restoration
2026-07-15 00:04:08 -07:00
Saoud Rizwan 5ef3b81369 feat(desktop): improve chat markdown rendering (#12276)
* feat(desktop): refresh navigation and visual foundation

* feat(desktop): align settings with Cline Hub

* feat(desktop): improve chat markdown rendering

* fix(desktop): resolve review feedback blockers

* fix(desktop): refine inline code sizing

* fix(desktop): preserve workspace choices during startup

* fix(desktop): restore sidebar session sorting

* fix(desktop): harden session startup state

* fix(desktop): tighten settings navigation state

* fix(desktop): tighten markdown link handling

* test(desktop): cover late workspace restoration
2026-07-14 23:49:22 -07:00
Saoud Rizwan ec3a57771d fix(cli): block compaction during active turns (#12296) 2026-07-14 23:23:09 -07:00
Saoud Rizwan a695dab23a feat(desktop): align settings with Cline Hub (#12275)
* feat(desktop): refresh navigation and visual foundation

* feat(desktop): align settings with Cline Hub

* test(desktop): support webview component tests

* fix(desktop): resolve review feedback blockers

* fix(desktop): preserve workspace choices during startup

* fix(desktop): restore sidebar session sorting

* fix(desktop): harden session startup state

* fix(desktop): tighten settings navigation state

* test(desktop): cover late workspace restoration

* chore: preserve upstream merge contents
2026-07-14 23:18:48 -07:00
Saoud Rizwan 77af52661c fix(telemetry): attach organization context to cached-credential identity (#12288)
* fix(telemetry): attach organization context to cached-credential identity

CLI cached credentials only stored the account id, so telemetry identity
resolved from them (headless runs via #11581, the hub daemon via #12177)
carried user_id but no organization_id - making CLI/hub usage invisible
to organization-scoped dashboards even where per-user attribution works.

- AuthSettingsSchema gains optional organizationId/organizationName/
  memberId
- loadClineAccountSnapshot persists the active organization into the
  cached cline provider settings after fetching /me (cleared when the
  user is on their personal account), so the context survives across
  processes without a network call
- the CLI runtime identify and the hub daemon identity refresh read the
  persisted fields and pass them to identifyAccount; the daemon re-keys
  its refresh on account+organization so an org switch re-identifies a
  long-lived daemon

* fix(telemetry): strip stray NUL byte, drop needless reshaping of daemon identity resolve
2026-07-14 23:10:27 -07:00
Saoud Rizwan 0f4acccd08 feat(desktop): refresh navigation and visual foundation (#12268)
* feat(desktop): refresh navigation and visual foundation

* test(desktop): support webview component tests

* fix(desktop): resolve review feedback blockers

* fix(desktop): preserve workspace choices during startup

* fix(desktop): restore sidebar session sorting

* fix(desktop): harden session startup state

* test(desktop): cover late workspace restoration
2026-07-14 23:02:06 -07:00
Bee f8c73cd8cc feat(core): persist and refresh workspace git info (#12295) 2026-07-15 07:30:16 +02:00
Max 55a31a0d8a feat(vscode): add rollout telemetry to SDK extension (#12292)
* feat(vscode): add shared rollout telemetry contract

* feat(vscode-sdk): propagate rollout metadata

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-14 22:29:52 -07:00
Bee 04438c0d54 feat: shows compaction progress status in UI (#12137)
* feat: shows compaction progress status in UI

* fixes p2

* fix: complete compaction lifecycle delivery

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-15 13:09:10 +08:00
Bee f053ec48e4 refactor(llms): owns provider-specific header policy (#12187)
* refactor(llms): owns provider-specific header policy

* header
2026-07-15 03:23:23 +02:00
Bee 0df406723c refactor(core): normalize read file request path aliases (#12287)
* fix(core): normalize read file request path aliases

Accept `file_path` and `filePath` in read file requests and normalize them to the canonical `path` field. Apply alias handling to direct, array, and nested inputs to prevent model-generated variants from failing validation.

Clarify path descriptions by removing redundant wording.

* update test
2026-07-15 08:46:00 +08:00
Dominic Cooney 12703bf407 Improve VS Code terminal reliability: OSC 633 parser, exit codes, timeout handling (#11972) 2026-07-14 16:34:36 -07:00
Bee 4a97b46f5f refactor(core): simplifies context compaction trigger (#12217)
* refactor(core): simplifies context compaction trigger

Simplifies automatic context compaction so it always triggers when input usage reaches 80% of the model’s effective maximum input-token limit.

* add bound

* feedback apply

* complete

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-15 02:11:38 +08:00
Saoud Rizwan 36fc3327ac fix(cli): highlight API key fallback hint (#12283) 2026-07-14 10:51:05 -07:00
Max 2b48dc411f make old tasks incompatible with new cline extension (#12127)
Preserve pretty legacy task display after resume

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-14 10:00:23 -07:00
Tomás Barreiro da6fe718d0 Rename sessionStartedAt to sessionStartedAtMs (#12279) 2026-07-14 16:08:17 +02:00
Tomás Barreiro 3515333e23 Review against camelCase telemetry (#12280) 2026-07-14 15:59:19 +02:00
Saoud Rizwan bd9ac5872b ci(sdk): create GitHub release and post to Slack on latest SDK publish (#12223)
* ci(sdk): create GitHub release and post to Slack on latest SDK publish

* ci(sdk): use random heredoc delimiter for changelog output
2026-07-14 01:31:45 -07:00
Saoud Rizwan fb15324ad2 fix(cli): prevent use-after-free when setting terminal title during TUI teardown (#12229)
* fix(cli): prevent use-after-free when setting terminal title during TUI teardown

* fix(cli): re-check renderer destruction before title reset in teardown microtask

* test(cli): cover terminal title teardown lifecycle
2026-07-14 01:31:01 -07:00
Saoud Rizwan 7a27c04ffa fix(core): stop reporting benign git states as workspace init errors (#12189)
* fix(core): stop reporting benign git states as workspace init errors [ENG-2244]

A freshly initialized repo with no commits makes 'git rev-parse HEAD'
fail, which generateWorkspaceInfoWithDiagnostics recorded as a workspace
init error and surfaced as workspace.init_error telemetry on every
session bootstrap. Filter out git failures that reflect normal
repository states; genuine failures (missing directory, real git
breakage) are still reported.

* fix(core): drop 'bad revision' from benign git error filter

Review feedback: 'fatal: bad revision HEAD' can also indicate a corrupt
.git/HEAD (checkIsRepo still succeeds), which is a genuinely broken
workspace that should keep reporting. The remaining patterns cover the
empty-repo message variants.
2026-07-14 01:30:33 -07:00
Saoud Rizwan c37b252f65 fix(vscode): restore multi-root mention resolution and validate stored task cwd (#12190)
* fix(vscode): restore multi-root mention resolution and validate stored task cwd [ENG-2245][ENG-2244]

The SDK adapter's ensureWorkspaceManager() was a stub returning
undefined, which silently disabled multi-root file mention resolution:
parseMentions only searched the primary cwd, so @-mentions of files in
secondary workspace roots failed with not_found. Build a real
WorkspaceRootManager from the host's workspace folders (cached until
the folder set changes) via a new WorkspaceRootManager.fromPaths().

Also validate that a resumed task's stored cwdOnTaskInitialization
still exists before using it — stale paths (deleted/moved dirs) fed
git-based workspace init and produced init-error telemetry.

* fix(vscode): use JSON.stringify for workspace manager cache key

Review feedback: a delimiter-joined key is ambiguous for paths
containing the delimiter (and the previous separator was an embedded
NUL byte). JSON.stringify is unambiguous and order-preserving.

* test(vscode): cover stored task cwd validation
2026-07-14 01:30:21 -07:00
Saoud Rizwan 2872138900 feat: suggest model IDs from OpenAI-compatible endpoints in extension and CLI (#12231)
* fix(vscode): use the requested provider's stored credentials when listing OpenAI-compatible models

The OpenAI-compatible settings pane already fetches GET <baseUrl>/models to
suggest model IDs, but the host handler always read the built-in "openai"
provider's stored settings. Custom OpenAI-compatible providers only expose a
masked API key to the webview, so their model-list requests went out
unauthenticated and the suggestion dropdown stayed empty.

Add provider_id to OpenAiModelsRequest and read that provider's stored API
key and custom headers in refreshOpenAiModels. Old clients omit the field,
which defaults to "openai" and preserves the previous behavior.

* feat(cli): suggest model ids from OpenAI-compatible endpoints in the model picker

The CLI showed a bare free-text input for openai-compatible providers and
never asked the endpoint what it serves. Fetch GET <baseUrl>/models with the
provider's stored API key/headers when opening the picker; when the endpoint
answers, show the standard fuzzy list (which keeps the "Create custom model
ID" row for manual entry). Any failure or empty answer falls back to the
existing free-text input.

* fix: resolve OpenAI-compatible model discovery config
2026-07-14 01:26:48 -07:00
Saoud Rizwan dc4620c529 fix(vscode): add to system prompt about plan/act modes and nudge about mode switches (#12227)
* feat(shared): move plan/act mode prompt instructions into the shared prompt builder

The CLI's #12057 fixes (mode-tag explanation, plan-mode contract,
mode-switch notice tracker) were CLI-only wiring, so the VSCode extension
never told the model what the <user_input mode> attribute means and plan
mode kept mutating files (CLINE-2576, CLINE-2607, CLINE-2579). Promote
the pieces every host needs into @cline/shared:

- buildClineSystemPrompt now appends MODE_TAG_INSTRUCTIONS for every mode
  and PLAN_MODE_INSTRUCTIONS for plan sessions, composed into the rules
  slot in the exact order the CLI historically built by hand, so CLI
  output is byte-identical after the refactor.
- The plan-mode contract gains an explicit run_commands paragraph:
  the tool intentionally stays available in plan mode (essential for
  read-only investigation) but is inspection-only there -- no file
  mutations, no state-changing commands. The mitigation for plan-mode
  mutations is prompting plus mode-switch notices, not tool removal.
- createModeSwitchNoticeTracker moves from apps/cli/runtime/interactive
  to @cline/shared next to formatModeSwitchNotice; the CLI re-exports it
  so its import surface and tests stay unchanged.
- deriveTitleFromPrompt gets a regression test pinning that titles never
  pick up mode-notice text.

* fix(vscode): teach the model about plan/act modes and surface mode switches

Port the CLI's #12057/#12058 plan-mode fixes to the extension:

- The session factory drops its local PLAN_MODE_INSTRUCTIONS copy; the
  shared prompt builder now emits both the mode-tag explanation and the
  plan-mode contract (including the read-only run_commands rule), so the
  extension's system prompt finally explains the <user_input mode>
  wrapper its own messages have carried all along.
- Manual Plan/Act toggles record a mode-switch notice in
  SdkModeCoordinator (shared round-trip-cancelling tracker, scoped to
  the rebuilt session so it never leaks across tasks), recorded only
  after the session replacement actually commits. The model-initiated
  switch_to_act_mode path passes source: "tool" and records nothing,
  matching the CLI: its tool result and continuation prompt already
  announce the switch.
- SdkSessionLifecycle.fireAndForgetSend -- the single funnel for
  outbound turn sends -- consumes the notice and prepends
  formatModeSwitchNotice() to the next message, exactly like the CLI's
  run-interactive stamping.
- Display boundaries never render the raw tag: the queued-prompt echo
  in the message translator now goes through formatDisplayUserInput,
  and isSyntheticUserPrompt strips notices before matching so a stamped
  continuation prompt cannot shift edit/regenerate ordinals.
2026-07-14 01:26:20 -07:00
Saoud Rizwan 2ac5c85e69 fix(vscode): restore editor diff view for SDK edit tools (#12219)
* feat(sdk): expose edit-executor internals for host diff previews

Extract computePatchChanges() from createApplyPatchExecutor so hosts can
compute a patch's per-file proposed content without writing to disk
(behavior-identical refactor; the executor now calls the helper), and
widen the @cline/core root exports with createEditorExecutor,
createApplyPatchExecutor, computePatchChanges, PatchActionType and the
related types. Needed by the VS Code adapter to restore the editor diff
view for SDK edit tools.

* fix(vscode): restore editor diff view for SDK edit tools

Adds SdkDiffEditCoordinator, which owns per-toolCallId diff sessions over
the legacy DiffViewProvider abstraction (HostProvider factory, so the
external/JetBrains gRPC DiffService path keeps working):

- the diff editor opens populated before the approval ask renders (the
  SDK surfaces tool input only after the model stream completes, so the
  approval callback is the only pre-execution point with full input)
- an overridden editor executor saves through the diff document:
  user edits in the editable right pane and post-save auto-formatting
  flow back to the model via formatResponse.fileEditWithUserChanges,
  plus 'new problems' diagnostics
- Reject/abort reverts (new files: file + created dirs removed)
- auto-approved edits open the diff during execution with the legacy
  3.5s diagnostics settle; Background Edit keeps the headless disk path
- apply_patch gets a preview-only diff of its first changed file; on
  approve the preview is reverted and the untouched SDK executor applies
  the whole patch
- any diff-pipeline failure reverts and falls back to the SDK disk
  executor, preserving canonical error strings

Fixes #11934 (CLINE-2580).

* refactor(vscode): make edit diff preview a read-only virtual-document diff

Reworks the diff view restoration after EDH testing showed the editable
real-document design breaking on same-file multi-edits (tab reuse opened
the actual file instead of a diff; sibling saves closed other sessions'
tabs; right-pane edits misbehaved).

New design per review:
- EditPreview abstraction (mirrors CommentReviewController pattern):
  VscodeEditPreview renders vscode.diff with BOTH sides as virtual
  cline-diff documents (unique fragment per preview, so same-file edits
  get distinct tabs and close is an exact tab match, never the real
  file); ExternalEditPreview uses the existing openMultiFileDiff/
  closeAllDiffs host-bridge RPCs. New createEditPreview factory on
  HostProvider.
- The preview never touches disk: executors close the preview and
  delegate to the SDK's default disk executors, whose results and error
  strings reach the model unchanged. Reject/abort just closes a tab.
- Dropped by design decision: editing in the diff view, user-edit
  feedback to the model, and diagnostics passback (the SDK already
  prompts the model to check).
- Auto-approved edits show a brief preview that lingers ~1.5s after the
  write; an abort cuts the linger short without failing the applied edit.
- A newer same-file preview supersedes an older pending one (approvals
  resolve sequentially), eliminating cross-session interference.
- Legacy DiffViewProvider stack returns to untouched dead code.

* fix(vscode): state that denied edits did not modify the file

Repro: ask Cline to edit a file, then answer the approval with feedback
instead of Approve/Reject. The denial reached the model as just
{"error":"make them bigger"} — nothing said the edit was NOT applied —
so the model treated the feedback as iteration on an applied change and
built its next old_text against content that never landed on disk. From
then on old_text no longer matched the real file and the diff preview
silently stopped appearing (and the eventual executor run would fail the
same way).

Denial reasons now come from buildToolApprovalDenialReason(): edit tools
get 'The user denied this edit. The file was NOT modified and still
contains its original content.' (legacy parity), and all tools get user
feedback wrapped in <feedback> tags instead of the bare prompt as the
whole reason. isKnownToolApprovalDenial also matches the new edit-denial
marker so translator suppression keeps working.

* feat(vscode): simulated streaming animation for edit previews

Brings back the legacy 'yellow sweep' feel on the virtual diff preview.
The SDK only surfaces complete tool input, so this is a deliberate
simulation of the legacy streaming look (which legacy also showed when
it already had the full content in memory).

The sweep covers the whole file like legacy did, with diff-aware pacing:

- Park at the top: whole document under the faded-yellow overlay, cursor
  highlight on line 0, viewport pinned to the top, ~400ms hold so the
  animation unambiguously starts from the top.
- Zip through unchanged spans in small fast steps (~8 lines per 16ms
  frame, capped per span) so they read as continuous motion.
- Slow down through each change: one line per 45ms frame with a ~350ms
  minimum dwell per hunk so even a one-line change visibly pauses.
- Changed runs come from a real line diff (diffLines), so multi-hunk
  edits slow at EACH hunk and the gaps between hunks zip; pure deletions
  pause at the deletion point.
- Zip frames chase the cursor (InCenter) for continuous scroll; typing
  frames scroll only when leaving the viewport (no per-frame judder).
- After the sweep reaches the bottom: short beat, then settle centered
  on the first changed line for review.

Mechanics: edit previews move from base64-query cline-diff URIs to a new
mutable cline-edit-preview content provider (content set programmatically,
re-rendered via onDidChange) so the virtual right side can update in
place. DecorationController is reused as-is. The approval ask renders
while the animation plays (legacy simultaneity); close() cancels
mid-animation; files >3000 lines render the final diff immediately.
External hosts keep the static openMultiFileDiff preview.

* chore(vscode): remove test artifact comment from memory-monitor

* fix(vscode): address review nits — skip diff computation for large files, close partially-opened previews

- buildEditPreviewAnimation (which runs a full line diff) now runs after
  the MAX_ANIMATED_LINES guard; oversized files use a cheap prefix scan
  just to aim the viewport.
- If preview.open() throws after partially opening, the tab is closed
  directly — the session was never registered, so discardPreview could
  not have reached it.

* fix(vscode): keep tsconfig valid JSON for test setup

* fix(vscode): bound diff preview animation
2026-07-14 01:25:36 -07:00
Tomás Barreiro ab68fd7f34 Store startedAt in auth metadata when starting a Cline session (#12270)
* Store startedAt in auth metadata when starting a Cline session

* Inject the sessionStartedAt when creating the auth credentials

* Remove injecting sessionStartedAt when it's not stored already

* Address review

* fix merge inconsistencies
2026-07-14 03:50:00 +02:00
Saoud Rizwan b4ed8a226e chore(cli): release v3.0.40 2026-07-13 12:25:49 -07:00
Saoud Rizwan cbf40961db fix(hub): make markdown code component assignable to streamdown Components
The custom MarkdownCode node type used a narrow { metastring?: string }
shape that is not assignable from the hast Element passed by
react-markdown/streamdown, so a clean rebuild (fresh dependency resolve,
as done by the release version.ts) fails the `satisfies Components`
check. Widen node.properties to Record<string, unknown> and validate the
metastring value at read time.
2026-07-13 12:02:49 -07:00
Saoud Rizwan 2d05ba52da chore(sdk): release v0.0.60 2026-07-13 11:25:21 -07:00
Saoud Rizwan 5d3778b5cf feat(cli): manual API key escape hatch for Cline OAuth providers (#12254)
* feat(cli): manual API key escape hatch for Cline OAuth providers

Add a way to configure the cline / cline-pass providers with a dashboard
API key from the /settings provider flow, for users where OAuth login
isn't working:

- "Enter API key manually" option in the already-configured dialog
- K keybinding in the OAuth login dialog to switch to key entry
- Saving clears stored OAuth tokens (on both the shared cline storage
  entry and any direct cline-pass entry) since the auth handler prefers
  auth.accessToken over apiKey — a stale token would otherwise keep
  winning over the manual key
- isProviderConfigured now counts a persisted API key for OAuth
  providers so escape-hatch users aren't forced back into OAuth on
  every provider switch

* fix(cli): move API key fallback to OAuth dialog
2026-07-13 11:14:11 -07:00
Saoud Rizwan 8d0eb54a1f feat(telemetry): track auth refresh outcomes to measure the hard-logout fix (#12256)
* fix(sdk): stop misclassifying transient refresh failures as invalid_grant

getValidClineCredentials returned null for BOTH a rejected refresh token and
any transient error (network down, timeout, 5xx) that happened to land after
the access token expired. Callers treat null as 'session dead' — the
extension wipes providers.json over it, logging out every Cline process on
the machine, which is what CLI users then hit as 'Unauthorized: please
re-authenticate'. A laptop waking from sleep past the ~1h token expiry with
a background job (balance/banners/remote-config) refreshing before the
network is up was enough to trigger it — no refresh-token rotation involved.

Now: null means the refresh token was REJECTED (re-auth required); transient
failures throw so callers keep stored credentials and retry later. The
extension's refreshAccessToken catch and the CLI's error surface already
handle the throw correctly with no changes.

* fix(sdk): write providers.json atomically

providers.json was written with a bare writeFileSync while being read
concurrently by every other Cline process (CLI, extension, hub). A reader
catching a partial write parses garbage, which read() silently treats as
EMPTY settings — indistinguishable from being logged out — and any
subsequent save from that process persists the empty state, erasing every
configured provider.

Stage to a pid-unique temp file and rename into place; rename is atomic on
POSIX and replaces on Windows, so readers only ever see a complete file.

* feat(telemetry): track auth refresh outcomes to measure the hard-logout fix

Adds the observability needed to verify in production that the
transient-vs-invalid_grant fix is working, and to diagnose any logouts that
remain:

- user.auth_refresh_soft_failure — fires when a refresh fails for a reason
  that does NOT invalidate the session (network error, timeout, 5xx) and
  stored credentials were kept. Instances with tokenExpired=true were hard
  logouts before the fix, so this is the 'prevented logout' counter. Emitted
  from the SDK (CLI path) and from the extension's refresh/restore catches
  under the same event name so dashboards aggregate both clients.
- user.auth_logged_out now carries the HTTP status and errorCode that caused
  it, and the extension emits it (with a distinct reason) at every site that
  clears providers.json: refresh_rejected, restore_refresh_rejected, and
  handleDeauth's LogoutReason (user_initiated / cross_window_sync / …), which
  was previously accepted and ignored. Extension-triggered logouts were
  completely invisible before — including the legacy-extension cross-window
  cascade, which this now measures directly.

Success looks like: auth_logged_out volume drops after release while
auth_refresh_soft_failure appears in its place, and any remaining logouts
carry a reason/status we can act on.

* fix(telemetry): route auth refresh events through SDK
2026-07-13 11:13:05 -07:00
Saoud Rizwan a3989acc38 fix(sdk): don't log users out when token refresh fails due to network/server errors (#12255)
* fix(sdk): stop misclassifying transient refresh failures as invalid_grant

getValidClineCredentials returned null for BOTH a rejected refresh token and
any transient error (network down, timeout, 5xx) that happened to land after
the access token expired. Callers treat null as 'session dead' — the
extension wipes providers.json over it, logging out every Cline process on
the machine, which is what CLI users then hit as 'Unauthorized: please
re-authenticate'. A laptop waking from sleep past the ~1h token expiry with
a background job (balance/banners/remote-config) refreshing before the
network is up was enough to trigger it — no refresh-token rotation involved.

Now: null means the refresh token was REJECTED (re-auth required); transient
failures throw so callers keep stored credentials and retry later. The
extension's refreshAccessToken catch and the CLI's error surface already
handle the throw correctly with no changes.

* fix(sdk): write providers.json atomically

providers.json was written with a bare writeFileSync while being read
concurrently by every other Cline process (CLI, extension, hub). A reader
catching a partial write parses garbage, which read() silently treats as
EMPTY settings — indistinguishable from being logged out — and any
subsequent save from that process persists the empty state, erasing every
configured provider.

Stage to a pid-unique temp file and rename into place; rename is atomic on
POSIX and replaces on Windows, so readers only ever see a complete file.
2026-07-13 08:58:26 -07:00
Saoud Rizwan d199b1bff9 fix(desktop-app): allow loopback origins for Next dev resources (#12251)
Next 16 blocks dev-resource requests (/_next/webpack-hmr, dev fonts) from
origins that don't match the dev server's own hostname. Browsing the web
dev mode via 127.0.0.1 left the page hanging with 'Blocked cross-origin
request to Next.js dev resource' warnings. allowedDevOrigins is dev-only,
so production/Tauri builds are unaffected.
2026-07-12 22:18:45 -07:00
Saoud Rizwan d41eed1198 feat(desktop-app): env-configurable sidecar bind host, trusted origins, and webview WS endpoint (#12250)
* feat(desktop-app): env-configurable sidecar bind host, trusted origins, and webview WS endpoint

Allows running the desktop app's web dev mode (dev:web + dev:sidecar) inside
a Docker container with published ports:

- CLINE_SIDECAR_HOST: sidecar bind hostname (default remains 127.0.0.1)
- CLINE_SIDECAR_TRUSTED_ORIGINS: comma-separated extra browser origins for
  the sidecar's origin allowlist (validation itself stays on)
- NEXT_PUBLIC_SIDECAR_WS_ENDPOINT: overrides the webview's hardcoded
  ws://127.0.0.1:3126/transport fallback so a browser on the Docker host can
  dial the published port

All defaults are unchanged, so local/Tauri behavior is unaffected when the
env vars are absent. When bound to 0.0.0.0 the printed ready endpoint
advertises 127.0.0.1 since a wildcard bind is not dialable.

* chore(desktop-app): untrack next-env.d.ts

It was added to .gitignore previously but never removed from the index, so
it kept showing as modified: Next.js rewrites the routes.d.ts import path
depending on whether 'next dev' or 'next build' ran last. The file is
regenerated by Next on every dev/build run, and the app's typecheck
(tsconfig.dev.json) excludes webview/, so nothing needs it tracked.

* style(desktop-app): format SIDECAR_HOST declaration
2026-07-12 21:59:07 -07:00
Tomás Barreiro 6309971089 Add the ClinePass limit error to the CLI (#12191)
* Add the ClinePass limit error to the CLI

* Update apps/cli/src/runtime/run-agent.test.ts

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

* format code and improve instructions

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-07-11 02:41:55 +02:00
Saoud Rizwan 2d2c669421 fix(cli): reload provider config when switching models (#12232) 2026-07-10 16:57:50 -07:00
Dominic Cooney c5f146a418 Add debug logging for Cline credential lifecycle (ENG-2213) (#12000)
* fix(auth): add early SDK debug logging for Cline credential lifecycle (ENG-2213)

Adds targeted debug-level logging at key points in the Cline/Cline Pass
credential lifecycle to diagnose intermittent logout issues. Credentials
are never logged in cleartext; an 8-hex-digit SHA-256 hash is used instead.

The SDK has two logger layers:
1. ClineCore.logger — session-scoped, threaded from ClineCore.create({logger})
   into session config and the agent event bridge.
2. setSdkLogger() — early/module-level, for components that operate before
   or outside of ClineCore sessions: ProviderSettingsManager (constructed
   at startup), RuntimeOAuthTokenManager, and cline.ts auth functions
   (token refresh). These can't be reached by the session-scoped logger.

Both VS Code (common.ts) and CLI (main.ts) call setSdkLogger() once at
startup. When no logger is registered (or the host filters out debug),
every call is a no-op — logging is never collected in normal use.

Instrumentation points (SDK core, shared by both surfaces):
- ProviderSettingsManager.read(): logs provider IDs, last-used, and whether
  Cline auth is present (with hashed access/refresh token fingerprints)
- ProviderSettingsManager.saveProviderSettings(): logs the provider being
  saved, tokenSource, whether Cline auth was present before/after, and
  flags authDropped when a previously-present Cline auth block disappears
- RuntimeOAuthTokenManager.resolveProviderApiKeyInternal(): logs each
  decision point (no_settings, no_credentials, refresh_start, refresh_null,
  refreshed+saved, not_refreshed) with hashed token fingerprints
- cline.ts refreshClineToken(): logs the refresh request URL, response
  status/errorCode on failure, and new token hashes on success
- cline.ts getValidClineCredentials(): logs the outcome at each branch
  (no_current_credentials, still_valid, needs_refresh, invalid_grant,
  transient_failure_kept_current, transient_failure_expired)

VS Code extension (auth-service.ts):
- readClineCredentials/writeClineCredentials/clearClineCredentials: logs
  credential presence and hashes at each disk I/O point
- refreshAccessToken: logs refresh start, null result (cleared), changed
  (written), or unchanged outcomes
- fetchUserInfoFromApi: logs the GET /api/v1/users/me request and response
  status

What to collect when investigating:

VS Code extension:
- Open the "Cline" output channel (View -> Output -> select "Cline")
- Look for lines containing: [SdkAuthService], providers.read,
  providers.save, oauth.resolve, cline.refresh, cline.getCredentials
- Debug logging is emitted at the DEBUG level; it appears in the output
  channel when IS_DEV=true or in development builds

CLI:
- Set CLINE_LOG_LEVEL=debug environment variable before running cline
- Collect the log file at ~/.cline/data/logs/cline.cli.log (or the path
  set by CLINE_LOG_PATH)
- Look for the same event names as above

Files changed:
- sdk/packages/core/src/auth/auth-debug.ts (NEW): hashSecret,
  setSdkLogger, getSdkLogger, sdkDebug
- sdk/packages/core/src/auth/cline.ts: refresh/getCredentials logging
- sdk/packages/core/src/services/storage/provider-settings-manager.ts:
  read/save logging
- sdk/packages/core/src/runtime/orchestration/runtime-oauth-token-manager.ts:
  resolve logging
- sdk/packages/core/src/index.ts: export early logger utilities
- apps/vscode/src/sdk/auth-service.ts: credential lifecycle logging
- apps/vscode/src/common.ts: register SDK early logger
- apps/cli/src/main.ts: register SDK early logger

* fix(vscode): inline SDK debug metadata into log message string (ENG-2213)

* fix(auth): gate debug logging on CLINE_LOG_LEVEL at runtime (ENG-2213)

* fix(auth): use interpolated debug strings, remove log-level gating (ENG-2213)

* refactor: move early logger to sdk/packages/core/src/logging/early-logger.ts

* fix: address review feedback — early logger registration, log after write, remove getSdkLogger from public API

* fix(vscode): add ISO timestamps to all log lines

* fix core import

* fix import

* fix tests

---------

Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
Co-authored-by: BarreiroT <tomasmbarreiroi@gmail.com>
2026-07-10 23:40:46 +02:00
Saoud Rizwan 261ee4c313 fix(vscode): show requested line range on read-file chat rows (#12225)
The webview already knew how to render readLineStart/readLineEnd on
readFile tool rows, but the SDK message translator never populated
them, so successive ranged reads of the same file all rendered as
identical bare paths. Extract start_line/end_line from read_files
input (per-file and single-path forms) and render open-ended reads
(start_line only) as "start+".
2026-07-10 13:25:30 -07:00
Saoud Rizwan d45b051c04 fix(cli): detect bun global installs after symlink resolution in auto-update (#12224) 2026-07-10 12:15:26 -07:00
Bee 78c83cdf33 fix(cli): preserve session id when in same session (#12188) 2026-07-10 17:59:55 +08:00
Bee 3266121fa1 feat(desktop): add typography spec (#12215)
* feat(desktop): add typography spec

* remove unused background component
2026-07-09 19:34:28 -07:00
Bee 6467de65a2 fix(plugin): follow up fix for agent-squad (#12216)
Follow up on my last PR where the last commit revert the removal of the regex field from zod schema
2026-07-09 19:33:04 -07:00
Bee 65fe885638 fix(sdk): remove regex from zod schema for agent-squad plugin example (#12214)
* fix(sdk): remove regex from zod schema for agent-squad plugin example

The `HandoffPathInput` schema used negative lookaheads to reject absolute paths and `..` traversal segments. When converted to JSON Schema, this regex caused consumers without lookaround support to fail with `invalid JSON schema: regex lookaround is not supported`.

This change removes the lookaround-based regex from the published schema and moves those checks to runtime validation. It preserves validation for allowed characters, absolute paths, traversal segments, and maximum length while strengthening cross-platform directory containment checks using Node’s path utilities.

* add back logger examples
2026-07-10 10:19:27 +08:00
Bee c3033d6f13 fix(vscode): refreshGroqModels caused cacheReadsPrice undefined error (#12213) 2026-07-10 08:52:06 +08:00
Max cfb1327a1b fix vscode hmr not working (#12212) 2026-07-09 16:49:35 -07:00
Alex Taboada 264af96e1b fix(vscode): prevent infinite loading when initializing task with an image (#12203) 2026-07-09 18:16:52 +02:00
Robin Newhouse 10cb9bd97a Add compaction budget hardening (#12142)
* Add compaction budget projection contract

* Tighten budget projection contract types

* Tighten dropped block action paths

* Add pure compaction budget projection engine

* Fix budget projection truncation accounting

* Drop provider-native blocks during budget projection

* Recompute protected tail after thinking pruning

* Align budget projection test tool results

* Clean up budget projection fixture indentation

* fix(core): narrow compaction protected tail

* Fix budget projection action accounting

* Budget agentic compaction summary input

* Harden agentic summary budget fallback

* Align agentic compaction test tool result

* Align agentic file ops with projected input

* Budget basic compaction projections

* Clarify basic projection budget logging

* Align basic sanitization image expectation

* Align basic compaction budget expectation

* Emit compaction budget emergency telemetry

* Tighten compaction budget telemetry types

* Preserve compaction status notice reasons

* fix(core): account compaction tokens consistently

* fix(core): align skipped compaction token accounting
2026-07-09 02:15:55 -07:00
Saoud Rizwan 2ee18e7f0c chore(cli): release v3.0.39 2026-07-08 21:15:22 -07:00
Saoud Rizwan 0b65506a2b chore(sdk): release v0.0.59 2026-07-08 20:00:37 -07:00
Saoud Rizwan 3502608081 fix(telemetry): emit telemetry from the detached hub daemon process (#12177)
* feat(sdk): emit telemetry from the hub daemon process

The detached hub daemon hosts the LocalRuntimeHost that emits
task.conversation_turn and task.tokens for every hub-backed session
(CLI in prefer-hub mode, desktop app, connectors), but the daemon
entrypoint never created a telemetry handle - startHubWebSocketServer
received telemetry: undefined and every capture in the daemon-side
runtime was a no-op. Sessions billed normally on the backend while
reporting nothing to OTel.

- create a ConfiguredTelemetryHandle in the daemon entry and pass it to
  the websocket server and schedule runtime handlers
- identify from the cached cline account at startup and re-resolve
  periodically, since the long-lived daemon often starts before login
  or outlives an account switch
- flush and dispose the handle on graceful and fatal shutdown

* fix(sdk): flush daemon telemetry when server startup fails

If startHubWebSocketServer throws, dispose the telemetry handle before
rethrowing so failed daemon starts are visible in telemetry instead of
dying silently.

* fix(sdk): bound daemon telemetry flush and reuse settings manager

- Race dispose's flush against a 5s deadline so a hung exporter can't
  keep a crashed daemon alive holding the hub port (before this PR the
  daemon exited immediately on fatal errors; the flush must not change
  that materially).
- Construct ProviderSettingsManager once instead of every identity
  refresh; its constructor runs legacy-migration and provider
  registration side effects, and getProviderSettings re-reads the file
  per call anyway.
- Test the dispose-on-startup-failure path and the cline-hub-daemon
  platform metadata.

* fix(sdk): label daemon telemetry cline_type as hub

Review feedback from @abeatrix: daemon-hosted sessions can be triggered
by the CLI, desktop app, or connectors, so daemon-emitted events should
not share the CLI process's cline_type. Existing values are "cli" and
"VSCode Extension"; the daemon now reports "hub" (with the finer
platform=cline-hub-daemon kept as-is).
2026-07-08 19:35:26 -07:00
Saoud Rizwan ed3107f9ec Revert "docs: add Cline free models page (#12183)" (#12185)
This reverts commit 6bce48aad4.
2026-07-08 19:32:36 -07:00
Renee Huang 6bce48aad4 docs: add Cline free models page (#12183)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-08 19:22:32 -07:00
Bee 1e1b6af51c fix(sdk): set versioned Cline client-identity headers for Cline provider (#12182)
* fix(sdk): set versioned Cline client-identity headers for Cline provider

* address feedback

* feat: add platform metadata to client context

Include platform, platformVersion, and isMultiRoot in extension client
context for CLI, ACP, and VS Code sessions. This provides downstream
core/session logic with richer runtime information and distinguishes ACP
clients from the standard CLI client.

* lint

* clean up

* fix: resolve client host identity via HostProvider for standalone compatibility

cline-session-factory.ts is also bundled into the standalone cline-core
(JetBrains), where the 'vscode' module resolves to the generated Proxy-stub
module: vscode.env.appName and vscode.version return Proxy objects, which
would flow into X-PLATFORM/X-PLATFORM-VERSION header values and fail at
request serialization.

Resolve the identity through HostProvider.env.getHostVersion() instead —
the VS Code hostbridge returns the identical values (vscode.env.appName,
vscode.version, ClineClient.VSCode, extension version), and JetBrains'
hostbridge returns its real host values, so the standalone stops reporting
itself as the VS Code extension as a bonus. Multi-root detection goes
through HostProvider.workspace.getWorkspacePaths() for the same reason.
Both resolvers degrade gracefully (undefined/false) if the host bridge is
unavailable, in which case the header builder falls back to source-derived
values.

* Add unit test as proof

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-08 19:08:25 -07:00
Saoud Rizwan ee49900232 chore(greptile): update telemetry review rules for the monorepo layout (#12180)
The .greptile config was written for the pre-merge standalone cline/sdk
repo and never updated after the monorepo merge:

- the sdk-telemetry-doc-update rule enforced an Event Catalog in DOC.md,
  a file that does not exist in this repo (it now emits a false P2 on
  every PR touching core-events.ts, e.g. #12177)
- rules.md cited PR #357, apps/vscode/src/hub-daemon.ts, and
  apps/vscode/src/telemetry.ts - none of which exist here
- the 'Hub Daemon Metadata Forwarding' section described an argv-based
  metadata payload that was never implemented in this repo; replaced
  with the actual daemon-owned telemetry pattern from #12177
- the opted-out-test rule now describes the real convention: assert the
  event flows through capture (no-op for OptedOutTelemetryService), not
  captureRequired
2026-07-08 16:43:14 -07:00
Saoud Rizwan a1d5589d19 feat: allow selecting Cline free models on the ClinePass provider (#12140)
* feat(llms): include Cline free models in the cline-pass catalog

* feat(vscode): show Subscribed/Free model tabs on the ClinePass provider

* feat(cli): show Subscribed/Free sections in the ClinePass model picker

* fix(cli): drop redundant browse-all entry from ClinePass picker

* fix(cli): show only subscribed models in ClinePass onboarding picker

* feat(cli): include free models and quota explainer in ClinePass onboarding picker

* fix: shorten ClinePass free section copy

* fix(cli): strip redundant free markers from sectioned picker names

* fix: drop free from ClinePass free section copy

* fix: tighten ClinePass free section copy

* refactor: address review feedback on ClinePass free models

- single buildFeaturedModelEntries(providerId) dispatcher, builders private
- rename isClineProvider to isClineManagedProvider (includes cline-pass)
- use isClineManagedProvider in the free-model cost check
- themed tab border, pretty names on free model cards
- clearer cline-pass cost test name

* fix: address ClinePass free-model review blockers

- Stop re-sorting the cline-pass live catalog by release date in
  mergeKnownModels: free models carry OpenRouter release dates, so the
  sort could put a free model first and make it the fallback default
  when the bundled default id rotates out of the live clinePass bucket.
  Preserve the normalize-time order (pass models first) and pin it with
  an end-to-end resolveProviderConfig test.
- Add the browse-all escape to the CLI ClinePass picker when the
  clinePass bucket is empty (bundled fallback after a fetch failure),
  so a subscriber isn't left with a free-models-only picker.
- Rename ErrorRow's local isClineManagedProvider to
  isClineUsageBillingProvider: it only matches the cline provider,
  unlike the shared util of the same name that also matches cline-pass.
2026-07-08 15:08:27 -07:00
Tomás Barreiro 721fda2e99 Add ClinePass limit error (#12162)
* Add ClinePass limit error

* refactor regex

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-08 14:54:20 -07:00
Saoud Rizwan 5e78861eb5 fix(vscode): update ClinePass onboarding option copy (#12173) 2026-07-08 13:21:12 -07:00
Robin Newhouse 29798f59f3 Persist VS Code manual compaction sidecar (#11900)
* Persist VS Code manual compaction sidecar

* Fix compaction test isolation

* Address PR feedback on compaction comments

* Fix compaction test core mock hoisting

* fix(vscode): avoid compaction session rebuild

* fix(core): validate active compaction from persisted transcript

* fix(vscode): harden manual compaction sidecar flow
2026-07-08 13:18:22 -07:00
Ara 177d0eb07f Remove Cline model picker recommendation copy (#12170) 2026-07-08 12:15:53 -07:00
Bee 869a87a220 fix(core): use no-emit TypeScript config for checks (#12139)
* fix(core): use no-emit TypeScript config for checks

Update the core package TypeScript config to run checks without emitting files,
allowing broader workspace sources via the package parent rootDir. Simplify the
dev config so it only extends the main package config and avoids duplicated
compiler overrides.

* feedback

* remove dead code
2026-07-08 11:15:16 -07:00
Max 0cfd0bbe05 Fix VS Code F5 webview debug flow (#12027)
* fix vscode f5 settings

- fixed the hot module reloading issue while debugging the extension.
- also fixed issue where deb:webview task wasn't showing as complete

* fix vscode webview dev cleanup

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-07-08 10:53:41 -07:00
yanalialiuk 08f656532f docs: add Atomic Chat local provider setup guide (#11966)
* docs: add Atomic Chat local provider setup guide

Document Atomic Chat alongside Ollama and LM Studio in the local models
overview and add a dedicated provider configuration page.

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

* Update overview.mdx

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
2026-07-08 10:48:58 -07:00
Tomás Barreiro 10dece6677 Remove all ClinePass GLM 5.1 references (#12107)
* Remove all ClinePass GLM 5.1 references

* fix other references
2026-07-08 14:59:10 +02:00
Sufiyan Khan 885a2936b6 docs(authorizing): remove model-specific wording from generic setup step (#12156)
Step 4 in the IDE setup flow says 'Choose your desired Claude model'
but applies to all providers (OpenAI, Gemini, DeepSeek, local, etc.).
Drop 'Claude' to keep it provider-agnostic.
2026-07-08 21:22:33 +09:00
Dominic Cooney 90c427740d perf(sdk): stop listSessions hot loop from hanging the extension host (#11967)
* perf(sdk): stop listSessions hot loop from hanging the extension host

getStateToPostToWebview rebuilt the full task history on nearly every
streaming/session event, and each rebuild ran persistence-service.listSessions,
which synchronously read + Zod-parsed every session manifest. The 10s metadata
cache meant to absorb this was wiped on every per-turn updateTaskUsage, so each
state post paid the full synchronous scan, saturating the extension-host event
loop (observed as a tight listSessions/readFileUtf8 loop in CPU profiles).

- Debounce/coalesce postStateToWebview: trailing 50ms debounce plus a single
  queued follow-up so bursts collapse into one rebuild; dispose() tears it down.
- Add an async, title-only manifest reader (readSessionManifestTitle) and use it
  in listSessions to resolve titles concurrently off-thread, instead of a
  synchronous readFileSync + full SessionManifestSchema (Zod) parse per row. The
  existing sync manifest methods are left intact.
- On single-session updates, patch just the changed record in the merged-history
  cache in place instead of invalidating it, so frequent per-turn usage updates
  no longer force the next state post to re-enumerate and re-merge every session.

* refactor(sdk): strengthen session history cache patching

Replace patchMetadataHistoryCacheRecord (boolean-returning, metadata-only,
no re-sort) with updateCachedSessionRecord (void, updates prompt +
metadata + updatedAt, re-sorts via shared comparator).

- Void return eliminates the ignorable fallback contract.
- Mirrors all fields the persistence layer writes (prompt, metadata,
  updatedAt) so cache and disk stay consistent.
- Re-sorts after patching so the updated record bubbles to the correct
  position, using a shared compareSessionHistoryRecordsByRecencyDesc
  comparator also used by listHistory.
- Derives updatedAt from the HistoryItem timestamp instead of constructing
  a second clock value.
- Self-invalidates on cache miss so callers never manage the fallback.

Adds tests for in-place patching, re-sorting, per-turn usage hot path,
and cache-miss invalidation.

* fix(sdk): await in-flight state post during dispose

Greptile feedback: dispose() did not await a concurrently-running
runDebouncedStatePost, so an in-flight flushStateToWebview could access
torn-down resources after disposal.

Track the runDebouncedStatePost promise in statePostInFlightPromise.
In dispose(), after setting isDisposed and clearing the timer, await
the in-flight promise (swallowing errors) before tearing down downstream
resources. The !this.isDisposed guard in the loop prevents further
iterations after disposal.

* fix(sdk): address review feedback on state-post debounce and cache patch

Three issues from code review of the listSessions hot-loop fix:

1. dispose() could await the wrong promise. A second debounced timer
   firing while a flush was already running overwrote
   statePostInFlightPromise with a throwaway resolved promise from the
   join path, so dispose() could return while the original flush was
   still executing. Extract the debounce/coalesce state machine into
   StatePostDebouncer, and only track the promise from the call that
   actually starts a new flush loop.

2. postStateToWebview() swallowed flush errors, resolving every pending
   caller even when flushStateToWebview() threw. Callers awaiting
   postStateToWebview() now see the rejection, matching pre-debounce
   behavior.

3. Cache patching derived the cached updatedAt from HistoryItem.ts,
   but the persistence adapter always stamps updatedAt with the
   wall-clock write time. Callers like toggleTaskFavorite() reuse an
   old HistoryItem whose ts predates the write, which let the cached
   ordering diverge from disk until the 10s TTL expired. Stamp the
   cache patch with the write time instead.

Adds unit tests for StatePostDebouncer covering the dispose race and
error-propagation regressions, and a sdk-task-history test for the
stale-updatedAt cache-ordering regression.

* fix(sdk): don't patch cache when session update write didn't land

Beatrix's review feedback: updateSession() ignored the { updated:
boolean } result from host.update() and unconditionally patched the
metadata cache. When persistence returns updated: false (session
deleted/missing, or an optimistic-concurrency retry exhausted by a
racing writer), the webview could show a fake updated record until the
cache TTL expired.

Check the write result: only patch the cache when updated === true,
otherwise invalidate it so the next read re-enumerates from disk.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-07-08 13:23:18 +09:00
alex-lum e6028168f2 fix(sdk/cli): emit user_id in SDK/CLI telemetry identity attributes (CLINE-2406) (#11581)
* fix(sdk/cli): emit user_id in telemetry identity attributes

Per CLINE-2406, downstream analytics expects an explicit user_id field
in authenticated SDK/CLI OpenTelemetry log attributes.

Changes:
- sdk/packages/core/src/services/telemetry/core-events.ts: add
  user_id: account.id alongside the existing account_id in
  identifyAccount() updateCommonProperties call.
- sdk/packages/core/src/services/telemetry/core-events.test.ts: new
  identifyAccount suite verifying user_id, account_id, distinct_id, and
  org context fields for authenticated user without org, with active org,
  absent/blank id handling, and no-op when telemetry is undefined.
- apps/cli/src/main.ts: after loading Cline provider settings in the
  runtime path, read auth.accountId and call identifyTelemetryAccount so
  subsequent task.* and workspace.* events carry user_id. Document
  user.extension_activated as pre-auth by design for subcommand flows.
- apps/cli/src/main.test.ts: three new tests covering saved accountId
  triggers identity, missing accountId skips identity, non-Cline
  provider skips identity.

* fix(sdk/cli): address review feedback on telemetry identity

- Use trimmed distinctId for user_id in identifyAccount() to keep
  user_id and distinct_id consistent when IDs have whitespace
- Remove fragile type cast in CLI main.ts; ProviderSettings already
  exposes auth.accountId via AuthSettingsSchema
2026-07-07 19:20:03 -07:00
Bee c3f75b3ff0 chore: Cline Code Desktop App update (#12012)
* wip: Cline Code Desktop App

Add Bun/Tauri desktop packaging commands for macOS, Windows, and Linux, including output to dist/desktop. Enforce macOS signing and notarization requirements for shareable builds while allowing an explicit unsigned local test path.

Document desktop packaging prerequisites, ignore generated build artifacts, and wire runtime session connection updates needed by the desktop app.

Clean up and update sidecar functions.
Safe to merge as this is not a published app.

* fixes

* chat

* apply

* ClinePass support

* add build instructions and use system theme

* fix: diff status

* update tool calls display

* connection updates

* lint fix

* fix keydown
2026-07-08 09:08:34 +08:00
Bee 88ce3e0b11 fix(core): emit accurate str_replace diffs (#12102)
* fix(core): emit accurate str_replace diffs

* fixes
2026-07-07 16:02:17 -07:00
Bee dd719dce86 fix(llms): OpenAI Codex model metadata for GPT Subscription provider (#12129)
* fix(llms): OpenAI Codex model metadata for GPT Subscription provider

* add unit tests

* Update stale unit tests

* clarify doc string

* Update docs format

* update old test
2026-07-07 15:57:21 -07:00
Robin Newhouse d5db7eb853 Preserve canonical session history during compaction ENG-1967 (#10651)
* Preserve canonical history with compaction sidecar

* Clarify prepareTurn request projection semantics

* Harden hub compaction sidecar ownership

* Handle compaction sidecar edge cases

* Address compaction sidecar review feedback

* Tighten compaction sidecar safety

* Extract atomic session file writes

* Assert compaction boundary role delimiter

* Simplify compaction source hashing

* Fix compaction smoke test type guard

* Fix async interactive runtime tests

* Avoid dangling compaction path in manifests

* Address compaction sidecar review nits

* fix(cli): await async runtime helper in restart test
2026-07-07 13:19:05 -07:00
Max 11d5ebe8bc chore: schedule nightly VS Code extension publish (#12124)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-07-07 10:30:04 -07:00
Saoud Rizwan 6f7cc4907f chore(cli): release v3.0.38 2026-07-06 19:08:18 -07:00
Saoud Rizwan 27e3541569 chore(sdk): release v0.0.58 2026-07-06 18:52:52 -07:00
Bee ae9c5b4d9d fix(core): tolerate orphan line-range entries in read_files input (#12104)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-06 18:48:50 -07:00
Bee f86ca6b36b fix(test): fix stale palette tests (#12105)
Commit 9a9300846 ("restyle chat input…", which folded in PR #12075 "replace cyan accent with new plan/act palette") deliberately rebranded the TUI accents in palette.ts:

Dark act: ANSI "cyan" → #79b8ff (and plan "yellow" → #ffea7f, success "brightGreen" → #99e89b)
Light act: #0969da → #0f72cb (and plan #9a6700 → #867100), re-derived in OKLCH to keep the same hue as the new dark accents with ≥4.5:1 contrast on white
But palette.test.ts:27-35 still asserts the old values ("preserves the existing named ANSI colors" — a test description that's now literally obsolete). So getModeAccent("act", "dark") correctly returns #79b8ff, and the test expecting "cyan" fails.

The fix is to update the two tests to the new palette values (and rename the first test, since the colors are no longer named ANSI colors).
2026-07-06 18:23:49 -07:00
Saoud Rizwan 0e0b11032e chore(sdk): release v0.0.57 2026-07-06 18:06:43 -07:00
Saoud Rizwan a93d850aee feat(cli): tint assistant markdown accents by the mode they were produced in (#12101)
* feat(cli): tint assistant markdown accents by the mode they were produced in

Markdown's prominent elements (headings, bold, list markers, links) were
hardcoded to the act accent. getSyntaxStyle now takes the entry's mode
and colors those elements with the matching accent -- plan segments
render yellow-tinted markdown, act segments blue -- completing the
per-mode transcript coloring. Code token colors stay constant across
modes; styles are cached per theme+mode pair. Unstamped entries follow
the current mode, same fallback as the glyph accent.

* fix(cli): resolve entry mode once for glyph and markdown accents

Address review: the accent and mode props used parallel fallback chains
that could drift; both now derive from a single resolved entryMode. Also
cover the light-theme plan/act markdown accents in tests.
2026-07-06 17:21:45 -07:00
Saoud Rizwan fe25258b0d feat(cli): polish status bar usage display and ClinePass model name (#12077)
* feat(cli): restyle chat input with horizontal rules and slim user bubbles

Replace the tinted-background input field with a minimal frame: full-width
horizontal rules above and below the textarea and a bold accent-colored
prompt glyph, with no background fill. User message bubbles in the
transcript match the new look: a slim neutral-gray bar with the same
glyph, no vertical padding, and no mode-colored tint.

Palette gains getInputRuleColor (neutral adaptive mid-gray for the rules)
and getUserMessageBackground (neutral bubble tint), both built on a shared
OKLAB lift helper extracted from getModeInputBackground. Home view's
robot cursor-tracking offset is adjusted for the input's new left edge,
and the spacer line between the chat input and status bar is removed.

* feat(cli): replace cyan accent with new plan/act palette

Swap the TUI accent colors: act mode goes from ANSI cyan to #79b8ff and
plan mode from ANSI yellow to #ffea7f on dark themes. Light themes get
counterparts derived in OKLCH with the same hue but darkened to hold
>=4.5:1 contrast on white (#0f72cb act, #867100 plan), selected via the
existing getModeAccent theme switch.

All hardcoded "cyan" fg literals across dialogs, model selector, config
view, and onboarding now reference palette.act so the accent is a single
source of truth. The selection highlight follows the act color. The
subliminal OKLAB chroma nudge on input backgrounds/foregrounds now leans
blue (-a, -b) instead of cyan (-a, +b) to match the new accent hue.

* feat(cli): soften success green and use act accent in markdown

Swap the success/diff green from ANSI brightGreen and #22c55e to a muted
sage #87af87 on dark themes (auto-approve banner, git diff +stats, diff
view added-sign color); light themes keep the darker #116329 for contrast
on white. Markdown prominent elements (headings, bold, list markers,
links, table headers) now use the act accent via themePalette instead of
hardcoded one-dark cyan #56b6c2.

* feat(cli): harmonize dark syntax colors with brand accent palette

Rebuild the dark syntax highlighting family around the brand anchors:
functions use the act blue, strings and inline code use the success sage,
types/italics use a dimmed plan yellow, and the remaining hues (keyword
purple, variable coral, number orange, operator ice-blue) are regenerated
in OKLCH at the same pastel lightness/chroma weight (~L 0.78, C 0.11) so
code blocks read as part of the same palette. Light theme keeps its
GitHub-light set.

* fix(cli): brighten success green to match accent palette weight

#87af87 sat at roughly half the OKLCH chroma of the act/plan anchors and
read as gray next to them; #8bd28d (L 0.80, C 0.12) matches their weight.

* fix(cli): brighten success green a step further (#99e89b)

* feat(cli): polish status bar usage display and ClinePass model name

- Cost always renders with two decimals ($0.00) instead of switching to
  four decimals under a cent; the turn summary line drops its three-decimal
  format for the same reason.
- Token count next to the context bar is now just the number; the word
  'tokens' was redundant with the bar right beside it.
- Context window bar shrinks from 8 to 6 cells.
- ClinePass models resolve their friendly models.dev name like every other
  provider and get a (ClinePass) suffix: 'GLM 5.2 (ClinePass)' instead of
  'ClinePass/glm-5.2'.
- ClinePass no longer shows '$0.00 (included with subscription)' -- cost is
  simply hidden for subscription providers.

* fix(cli): place ClinePass suffix after reasoning effort in model name

* fix(cli): format ClinePass model name as 'ClinePass: <model>' prefix

* feat(cli): color transcript entries by the mode they were produced in (#12083)

* feat(cli): color transcript entries by the mode they were produced in

Previously the whole transcript retinted to the current mode's accent on
every plan/act toggle. Entries now record the agent mode active when
they were produced and keep that accent permanently, so a session reads
as a visible history of plan (yellow) and act (blue) segments.

How the mode is captured:
- Live sessions: appendEntry in SessionProvider stamps entries from a
  uiMode ref, covering every creation site including mid-run
  switch_to_act_mode flips (which already call setUiMode through the
  runtime dialog bridge).
- Resumed sessions: hydrateSessionMessages recovers the mode from the
  persisted <user_input mode="..."> wrappers via a new shared
  parseUserInputMode helper, and flips to act at switch_to_act_mode tool
  calls. Transcripts without wrappers stay unstamped and keep the
  current-mode fallback accent, matching the old behavior.
- Restores: the /history resume and checkpoint-restore paths insert
  hydrated history via replaceEntries instead of appendEntry loops, so
  live-entry stamping cannot overwrite hydration's stamps (which would
  lock resumed transcripts to the resume-time accent).

The load-bearing core fix: readPersistedMessagesFile stripped the
user_input wrappers and mode notices from user text on every read
('display sanitization'). That read path also feeds session restarts
(mode toggle, compaction-mode change, model change, fork, recovery),
which re-persist what they read -- so every restart laundered the mode
markers off disk and out of the model's seeded context, leaving nothing
for hydration to recover. Reads now return persisted messages verbatim
and formatting is the display surface's job: the CLI TUI, history
titles, and the VS Code SDK history loader already formatted at their
boundaries; the cline-hub webview history mapping and the CLI HTML
export (which used normalizeUserInput and leaked mode_notice text) now
do too. Connectors only surface assistant text, and the remaining
readMessages consumers are programmatic (usage math, re-seeding,
compaction input) where raw is correct.

* fix(shared): match parseUserInputMode exactly to what the writer emits

Drop the case-insensitive flag and the 'zen' value from the wrapper
regex: formatUserInputBlock only ever writes lowercase act/plan/yolo, so
anything else the parser accepted (uppercase look-alikes in adversarial
content, a zen value with no writer) could never be real persisted data.
2026-07-06 16:00:17 -07:00
Saoud Rizwan 53d1567731 feat(cli): default thinking level picker cursor to Medium instead of Off (#12092)
* feat(cli): default thinking level picker cursor to Medium instead of Off

* chore(cli): drop explanatory comments from thinking level defaults
2026-07-06 15:51:40 -07:00
Saoud Rizwan 9a93008463 feat(cli): restyle chat input with horizontal rules and slim user bubbles (#12074)
* feat(cli): restyle chat input with horizontal rules and slim user bubbles

Replace the tinted-background input field with a minimal frame: full-width
horizontal rules above and below the textarea and a bold accent-colored
prompt glyph, with no background fill. User message bubbles in the
transcript match the new look: a slim neutral-gray bar with the same
glyph, no vertical padding, and no mode-colored tint.

Palette gains getInputRuleColor (neutral adaptive mid-gray for the rules)
and getUserMessageBackground (neutral bubble tint), both built on a shared
OKLAB lift helper extracted from getModeInputBackground. Home view's
robot cursor-tracking offset is adjusted for the input's new left edge,
and the spacer line between the chat input and status bar is removed.

* feat(cli): replace cyan accent with new plan/act palette (#12075)

* feat(cli): replace cyan accent with new plan/act palette

Swap the TUI accent colors: act mode goes from ANSI cyan to #79b8ff and
plan mode from ANSI yellow to #ffea7f on dark themes. Light themes get
counterparts derived in OKLCH with the same hue but darkened to hold
>=4.5:1 contrast on white (#0f72cb act, #867100 plan), selected via the
existing getModeAccent theme switch.

All hardcoded "cyan" fg literals across dialogs, model selector, config
view, and onboarding now reference palette.act so the accent is a single
source of truth. The selection highlight follows the act color. The
subliminal OKLAB chroma nudge on input backgrounds/foregrounds now leans
blue (-a, -b) instead of cyan (-a, +b) to match the new accent hue.

* feat(cli): soften success green and use act accent in markdown

Swap the success/diff green from ANSI brightGreen and #22c55e to a muted
sage #87af87 on dark themes (auto-approve banner, git diff +stats, diff
view added-sign color); light themes keep the darker #116329 for contrast
on white. Markdown prominent elements (headings, bold, list markers,
links, table headers) now use the act accent via themePalette instead of
hardcoded one-dark cyan #56b6c2.

* feat(cli): harmonize dark syntax colors with brand accent palette

Rebuild the dark syntax highlighting family around the brand anchors:
functions use the act blue, strings and inline code use the success sage,
types/italics use a dimmed plan yellow, and the remaining hues (keyword
purple, variable coral, number orange, operator ice-blue) are regenerated
in OKLCH at the same pastel lightness/chroma weight (~L 0.78, C 0.11) so
code blocks read as part of the same palette. Light theme keeps its
GitHub-light set.

* fix(cli): brighten success green to match accent palette weight

#87af87 sat at roughly half the OKLCH chroma of the act/plan anchors and
read as gray next to them; #8bd28d (L 0.80, C 0.12) matches their weight.

* fix(cli): brighten success green a step further (#99e89b)
2026-07-06 15:50:50 -07:00
Saoud Rizwan 678b0ae951 docs: polish README model table grammar and wording (#12098)
Claude-Session: https://claude.ai/code/session_017VJNCE1o6zzcVfpjpTVnt5

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-06 15:29:12 -07:00
cline-cloud[bot] 8b6f2cf0b7 Raise live catalog default input tokens (#11930)
* fix(llms): raise catalog default input tokens

* Lower live catalog default input tokens to 128000

* Lower compaction DEFAULT_MAX_INPUT_TOKENS to 128000

---------

Co-authored-by: Cline Bot <bot@cline.bot>
Co-authored-by: John Simone <john@cline.bot>
2026-07-06 10:48:51 -07:00
Saoud Rizwan 25ef0939cc chore(cli): release v3.0.37 2026-07-03 19:31:47 -07:00
Saoud Rizwan 4f770011e9 chore(sdk): release v0.0.56 2026-07-03 19:11:03 -07:00
Saoud Rizwan a1d69fee6f fix(llms): stop AI SDK from rejecting malformed tool calls before flexible tool executors can handle them (#12061)
* fix(llms): stop rejecting malformed tool calls before tools can handle them

Weak models emit tool calls with schema mismatches (bare string for a
string[] arg) or unparsable JSON. The AI SDK adapter rejected both
before execution, so the lenient union schemas in the core tool
executors never ran. Drop the strict validate callback (tools own input
validation) and add experimental_repairToolCall backed by the shared
jsonrepair parser for arguments that fail JSON parsing.

* docs(llms): fix stale comment referencing removed validate callback
2026-07-03 18:55:31 -07:00
Saoud Rizwan 3575b38122 fix(shared): stop deleting mode_notice from outbound prompts (#12058)
* fix(shared): stop deleting mode_notice from outbound prompts

The mode-switch notice from #12057 never reached the model:
prepareTurnInput sanitizes every outbound prompt with normalizeUserInput
before wrapping it, and #12057 put the mode_notice strip inside
normalizeUserInput -- so the host deleted the notice on every send. The
transcript confirms it: messages sent after a toggle carry the
user_input wrapper but no notice, and models asked about it confabulate
having seen one because the system prompt describes the tag.

Move the strip into a dedicated stripModeNotices() applied only at
display boundaries: formatDisplayUserInput (TUI hydration, title
inference), deriveTitleFromPrompt (session titles), and the TUI queued
prompt echo. normalizeUserInput now preserves notices, with a
regression test pinning the outbound behavior. Side benefit: notices
survive the message-builder history normalization and the pending
prompt queue, so queued sends deliver them too.

* docs(shared): correct formatModeSwitchNotice JSDoc after strip relocation

* refactor(shared): generalize notice stripping to stripTagElements

stripModeNotices becomes a thin policy wrapper (DISPLAY_HIDDEN_TAGS owns
the what-to-hide list in one place) over a generic stripTagElements that
removes whole elements for any tag list -- the remove-element counterpart
to xmlTagsRemoval. Call sites at display boundaries now carry comments
explaining why stripping happens there and not in normalizeUserInput,
which also sanitizes model-bound prompts.

* revert(shared): drop stripTagElements generalization, keep simple stripModeNotices

The generic tag stripper added API surface without a second use case;
stripModeNotices goes back to the direct implementation. The display-vs-
model call-site comments from the same commit stay.
2026-07-03 15:45:34 -07:00
Saoud Rizwan b5468e1227 feat(cli): make plan/act mode switches visible to the model (#12057)
* feat(cli): make plan/act mode switches visible to the model

The mode signal already rides on every user message via the
<user_input mode="..."> wrapper, but nothing ever told the model what
the attribute means, and a manual plan/act toggle produced no inline
signal at all -- only an invisible system prompt swap the model cannot
diff. Two additions:

- The CLI system prompt now explains the mode attribute (both modes,
  since after a switch the transcript still contains messages tagged
  with the other mode) and that the newest message's mode governs.
- A user-initiated toggle stamps the next user message with a
  <mode_notice> block marking the switch, e.g. "The user switched from
  act mode to plan mode before sending this message." Round trips that
  return to the mode the model last saw cancel out. The model-initiated
  switch_to_act_mode path is excluded: its continuation prompt already
  announces the switch.

The notice vocabulary lives in @cline/shared next to the user_input
wrapper it extends, and normalizeUserInput hides the whole element from
transcript display the same structural way it strips the wrapper tags.

* fix(shared): strip mode_notice elements without polynomial regex

CodeQL flagged the lazy dot-all pattern (js/polynomial-redos): with the
global flag, every unmatched opening tag re-scans to the end of the
string, which is quadratic on adversarial transcript content. Replace
it with an indexOf-based splice that removes matched elements in linear
time and leaves unclosed tags intact, with a regression test on 50k
repeated open tags.
2026-07-03 15:05:22 -07:00
Saoud Rizwan 10d1c41b7a fix(cli): prevent empty session from racing a mode-change restart (#12056)
restartWithMessages cleared startupPromise and tore down the active
session before the replacement registered, leaving a window with no
active session and no startup in flight. A message submitted in that
window (e.g. typed right after a plan/act Tab toggle) made ensureReady
boot a blank fresh session, which then won the active slot over the
restarted session carrying the conversation history -- the model
responded as if the conversation had just started.

Publish the restart itself as the in-flight startupPromise so any
concurrent ensureReady waits for the restart instead of booting an
empty session. The barrier is cleared once the restart settles,
keeping failed restarts retryable by the next ensureReady.
2026-07-03 14:10:12 -07:00
Saoud Rizwan b823358867 chore(cli): release v3.0.36 2026-07-03 13:39:40 -07:00
Saoud Rizwan b876945c6d fix(cli): end plan-mode run on switch_to_act_mode and auto-continue with act tools (#12054)
* fix(cli): end plan-mode run on switch_to_act_mode and auto-continue with act tools

The CLI's switch_to_act_mode tool only queued the mode change; it was
applied after the whole turn finished. The model kept running the rest
of the turn with plan-mode tools (no editor) despite the tool result
claiming it now had edit access, so it fell back to editing files via
run_commands (sed/heredocs).

Mirror the VS Code extension's approach: the switch tool now completes
the run (lifecycle.completesRun), the pending mode change rebuilds the
session with act-mode tools, and a canned continuation prompt resumes
the approved plan automatically. Pending mode changes are tagged with
their source (tool vs UI toggle) so a Tab toggle racing a natural turn
completion can never auto-start plan execution the user did not
approve. The synthetic continuation prompt is hidden from transcript
hydration, and the plan-mode prompt/tool description now warn that
switching immediately starts execution.

* refactor(cli): show act-mode continuation prompt on resume instead of filtering it

Displaying the synthetic user message honestly beats exact-string
matching at the display layer, which was brittle and did not cover
other transcript consumers anyway. The live TUI still never echoes it;
it only appears as a user bubble when resuming a session. A
synthetic-message marker plumbed through SendSessionInput is the
principled follow-up if hiding it becomes worth the SDK surface
change.

* Revert "refactor(cli): show act-mode continuation prompt on resume instead of filtering it"

This reverts commit 969a24f9c9.
2026-07-03 13:10:29 -07:00
Saoud Rizwan 453cdea040 chore(cli): release v3.0.35 2026-07-03 10:26:32 -07:00
Saoud Rizwan 091eccdfe2 test: update GLM 5.2 context window assertions for refreshed catalog 2026-07-03 10:13:23 -07:00
Saoud Rizwan a2a46ae600 chore(sdk): release v0.0.55 2026-07-03 09:56:58 -07:00
Saoud Rizwan 82c9e77de2 style: apply formatter to pre-existing drift 2026-07-03 09:56:52 -07:00
Robin Newhouse dfd0e022a4 Add VS Code SDK compaction strategy setting (#11892)
* Add VS Code SDK compaction strategy setting

* Move compaction strategy setting into SDK

* Preserve stub global settings on compaction update

* Keep ApiProvider settings as proto strings

* Address compaction strategy review feedback
2026-07-02 23:58:47 -07:00
Robin Newhouse f0ec6a35bb fix: advertise run commands as shell strings (#12038) 2026-07-02 21:44:25 -07:00
Morgan Carr c09d54f5a2 fix(cli): format structured commands in history export (#12023)
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
2026-07-01 15:53:27 -07:00
Tomás Barreiro 984d70a351 Add the subscription promo code when linking to the dashboard subscription page (#12019)
* Add the subscription promo code when linking to the dashboard page

* revert tests
2026-07-02 00:47:30 +02:00
Bee bbe7b6fd49 fix(hub): hydrate tool results in session message mapping (#12011)
* fix(hub): hydrate tool results in session message mapping

Map historical tool call/use and tool result blocks into webview tool events, including same-message results and following user result messages. Add tests to verify hydrated outputs and block ordering so restored sessions render completed tool interactions correctly.

* feedback
2026-07-01 14:28:13 -07:00
Bee be97d951fa fix: first-prompt truncation (#12022)
* Fix basic compaction first-prompt truncation

Issue: shallow sessions on high-output models such as OpenRouter MiniMax M3 could auto-compact immediately and reduce the initial task prompt to only the leading <user_input> wrapper. Harbor still passed the full task into Cline and session metadata retained it, but the model conversation could receive a truncated first message and respond that the request was empty or cut off.

Root cause: the output-runway target used maxInputTokens - maxTokens for every basic compaction. For MiniMax M3, maxTokens is nearly maxInputTokens, producing a tiny target. Basic compaction then used its last-resort first-user truncation path, and raw messages still contain the user_input envelope, so prefix truncation preserved the wrapper instead of the actionable task.

Fix: only use the output-runway target after the transcript has at least five user-assistant pairs, so early/shallow tasks use the normal trigger-based target. Also prevent first-user truncation unless that first user message alone exceeds the trigger budget, preserving normal first-turn prompts while still allowing genuinely oversized prompts to be reduced. Added regression tests for the MiniMax-style shallow prompt case and the oversized-first-prompt escape hatch.

* Fix compaction budget for huge-output models

Avoid collapsing context-derived input budgets when a model reports an output limit nearly equal to its context window, such as MiniMax M3. In those cases, treating context-output as the input budget causes auto-compaction to trigger on normal-sized prompts.

Only use contextWindow - maxTokens when the derived value remains at least half of the context window. Also simplify long-conversation basic compaction targeting to maxInputTokens * 0.5 instead of applying the default target ratio to maxInputTokens - maxTokens.

Adds regression coverage for MiniMax-style context-only metadata so an 18k-token prompt does not compact against an incorrectly collapsed 12k input budget.

* Guard compaction estimator against cumulative metrics

* Address basic compaction target review comments
2026-07-01 13:51:50 -07:00
Robin Newhouse c331a8f4b6 fix(core): use curated default for legacy provider migration (#12030) 2026-07-01 12:42:42 -07:00
Ara f180f1584d Add first-request failure telemetry (#11852)
* fix(vscode): capture first request failure telemetry

* Fix provider failure telemetry review feedback

* test(vscode): avoid extension host mock matchers

* fix(vscode): use session metadata for provider failure telemetry

* test(vscode): document pre-session auth telemetry skip

* fix(vscode): use turn-scoped provider failure gate

* fix(vscode): include cline pass in auth failure checks

* refactor(vscode): keep provider failure turn count in gate
2026-07-01 10:23:36 -07:00
Ara 9197d15abf feat(vscode): recognize Tencent TokenHub provider (#12028) 2026-07-01 09:58:45 -07:00
Ara 6c0d5c97b1 feat(llms): add Tencent TokenHub provider (#12014) 2026-07-01 09:58:10 -07:00
Dominic Cooney 9a8be88e85 fix(protos): self-heal protoc download when bun skips grpc-tools postinstall (#12003) 2026-07-01 08:31:04 +09:00
Ara 60f4a482ca Add onboarding intent telemetry (#11848)
* Add onboarding intent telemetry

* Track prompt submit intent from chat UI
2026-06-30 14:19:51 -07:00
John Choi dbe15202e1 fix(cli): update ClinePass tests for forced-enabled behavior (#11990)
#11986 (Forcefully enable ClinePass on the CLI) hardcoded isClinePassEnabled: true in session-runtime.ts, provider-catalog.ts, and main.ts and removed the ext-cline-pass feature-flag check, but left the corresponding tests asserting the old flag-driven / disabled behavior. They fail on main (and every branch that merges it).

- session-runtime.test.ts: expect getLastUsedProviderSettings called with isClinePassEnabled: true.

- provider-catalog.test.ts: drop the obsolete getBooleanFlagEnabled('ext-cline-pass') assertion (source no longer reads the flag) and its now-unused mock; keep the isClinePassEnabled: true expectation.

- main.test.ts: the ClinePass flag is no longer read during startup, so getBooleanFlagEnabled is never called. Re-target the 'seed identity before flags' ordering assertion at refreshCliFeatureFlagsInBackground (which is still invoked after seeding), and wire that mock through featureFlagMocks.
2026-06-29 22:35:52 +02:00
Bee 8d102db392 chore: remove console logs from compaction test (#11983)
Follow up on #11894, this PR removes the console logging code from the compaction unit test.

Co-authored-by: John Choi <97497948+johnwschoi@users.noreply.github.com>
2026-06-29 13:22:05 -07:00
Max 3dfd5dc31c fix(cli): recover missing interactive sessions on message reads (#11984)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-06-29 13:21:27 -07:00
John Choi 43ce9f3694 fix(vscode): exclude vitest-owned tests from the mocha integration compile (#11981)
The vscode test (Extension Integration Tests) job is red on main: the updateAutoApprovalSettings suite (added in #11929) is vitest-native but was being collected and run by the Mocha @vscode/test-cli runner, where vitest-only matchers (toHaveBeenCalledWith / toHaveBeenCalledOnce / not.toHaveBeenCalled) are not registered, failing with 'is not a function'.

build-tests.js already excludes bun:test-owned tests from the Node-based out/ tree (single source of truth for the runner split). Extend that same mechanism to also exclude vitest-owned tests (files importing from 'vitest'), so neither bun nor vitest suites are ever compiled into the mocha out/ tree. Verified locally: detection catches the state suite (123 non-mocha test files total) while preserving the existing 60 bun __tests__ exclusions. No coverage lost — these suites run under test:vitest / bun.
2026-06-29 12:51:52 -07:00
Tomás Barreiro f1c73fb48b Remove unused imports (#11988) 2026-06-29 12:39:17 -07:00
Tomás Barreiro abaa8383c4 Forcefully enable ClinePass on the CLI (#11986) 2026-06-29 21:31:51 +02:00
Renee Huang 3a5e372d73 docs: document ClinePass API usage (#11980)
* docs: add MiniMax M3, Qwen3.7 Max, Qwen3.7 Plus models to ClinePass page

* more updates

* explicitly direct to personal org

* making the clinepass page more detailed

* updates to cline provider wording

* docs: document ClinePass API usage

* chore: discard McpHub change from PR

* docs: simplify ClinePass model slug table

* updates

* nit
2026-06-29 10:59:02 -07:00
Saoud Rizwan cf3a59f0e2 chore(cli): release v3.0.34 2026-06-29 09:59:28 -07:00
Tomás Barreiro b3aee68ca5 Merge both options and remove credits link (#11973)
* Merge both options and remove credits link

* Remove import

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-29 09:48:57 -07:00
Tomás Barreiro cd8fd29063 Improve the ClinePass step wording (#11974)
* Improve the ClinePass step wording

* Use a blacklist instead

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-29 09:21:27 -07:00
Saoud Rizwan 7777d61311 fix(cli): suppress ClinePass notice after onboarding (#11975) 2026-06-29 09:20:35 -07:00
Renee Huang 64fc3f372e docs: add MiniMax M3, Qwen3.7 Max, Qwen3.7 Plus models to ClinePass page (#11849)
* docs: add MiniMax M3, Qwen3.7 Max, Qwen3.7 Plus models to ClinePass page

* more updates

* explicitly direct to personal org

* making the clinepass page more detailed

* updates to cline provider wording
2026-06-29 07:28:38 -07:00
Saoud Rizwan 4175677e71 chore(cli): release v3.0.33 2026-06-28 23:45:56 -07:00
Saoud Rizwan 4934450947 fix(cli): show ClinePass subscription URL fallback (#11961)
* fix(cli): show ClinePass subscription URL fallback

* fix(cli): move ClinePass URL fallback below options
2026-06-28 23:33:58 -07:00
Saoud Rizwan b0a2d8a223 fix(cli): hide ClinePass promo for ClinePass users (#11963)
* fix(cli): hide ClinePass promo for ClinePass users

* fix(cli): expand ClinePass subscription card

* fix(cli): tune ClinePass subscription card height
2026-06-28 23:33:19 -07:00
Saoud Rizwan d9f1d862a5 fix(cli): use adaptive plan accent for ClinePass prompts (#11962) 2026-06-28 23:12:30 -07:00
Saoud Rizwan f8d73f3811 chore(cli): release v3.0.32 2026-06-28 21:43:18 -07:00
Saoud Rizwan 9aac8340dc chore(sdk): regenerate bun.lock for v0.0.54
Align resolved workspace versions in bun.lock with the v0.0.54 package
bumps. bun pm pack substitutes workspace:* deps using the version
recorded in bun.lock, so a stale lock made packed inter-package deps
resolve to 0.0.53, failing check-publish and the node smoke test (which
then pulled the old published @cline/shared from npm).
2026-06-28 21:24:41 -07:00
Saoud Rizwan 242b5ebff6 chore(sdk): release v0.0.54 2026-06-28 20:54:47 -07:00
Tomás Barreiro 7ca41fdb7d Improve ClinePass onboarding UX (#11959)
* Prevent ClinePass onboarding flicker

* Make the clinepass step scrollable and remove details
2026-06-28 20:49:27 -07:00
Saoud Rizwan 000918989f fix(cli): make ClinePass subscription screen selectable (#11957) 2026-06-28 20:12:04 -07:00
Saoud Rizwan 9560b6d625 fix(cli): use ClinePass as one word consistently (#11956) 2026-06-28 18:41:26 -07:00
Saoud Rizwan c7304097a7 fix(cli): update ClinePass provider UI copy (#11953)
* fix(cli): update ClinePass provider UI copy

* fix(llms): rename Cline provider display name

* fix(cli): separate ClinePass billing links
2026-06-28 17:57:11 -07:00
Tomás Barreiro 674a6022ee Add an intermediate step before going to ClinePass model selection (#11947)
* Add an intermediate step before going to model selection

* fix type issues

* use allSettled

* fix(cli): serialize ClinePass subscription checks

* fix(cli): handle missing ClinePass plan as unsubscribed

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-28 16:13:25 -07:00
Saoud Rizwan dbf0775384 fix(llms): keep error detail extraction for Error instances (#11949)
PR #11928 added an `instanceof Error` branch to extractErrorMessage to
preserve transport-error wrappers (e.g. "fetch failed: SocketError: ...
(UND_ERR_SOCKET)"), but that branch regressed two cases:

- Generic SDK wrappers like "No output generated. Check the stream for
  errors." were prepended to the real cause instead of being dropped.
- Errors carrying structured detail on responseBody/detail/error fields
  surfaced the bland top-level .message ("Bad Request") instead of the
  detail ("Instructions are required").

The Error branch now drops known generic wrappers in favor of the cause
and extracts structured detail from the error's own fields, while keeping
the transport-wrapper concat behavior #11928 intended.
2026-06-28 15:27:46 -07:00
Saoud Rizwan e130b45eb0 feat(cli): promote Cline Pass in startup notice (#11948) 2026-06-28 13:14:27 -07:00
Tomás Barreiro 6f4dbae86f Improve the ClinePass onboarding experience on the CLI [ENG-2236] (#11946)
* Improve the ClinePass onboarding experience on the CLI

* Update apps/cli/src/tui/views/onboarding/screens.tsx

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

* address comments

* style(cli): format ClinePass onboarding warning

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-28 10:53:27 -07:00
Saoud Rizwan c7de31ae24 ci(vscode): gate legacy publish on test job (#11936) 2026-06-27 19:33:37 -07:00
Saoud Rizwan 45dddb9a4e ci(vscode): add legacy extension publish workflow (#11935) 2026-06-27 19:17:46 -07:00
Bee e32caee96b fix: basic compaction token budgeting (#11894)
* Improve compaction token budgeting

Use MessageWithMetadata.metrics input/output token counts when estimating message size for compaction, falling back to the existing chars/3 heuristic only when metrics are unavailable. This makes trigger decisions and post-compaction accounting use provider-reported token usage instead of relying only on serialized character estimates.

When an explicit compaction maxInputTokens budget is configured and the model exposes maxTokens, reserve half of the model output budget before triggering compaction. This gives the next provider request room for completion tokens and reduces edge cases where the local context estimate passes but the provider rejects the prompt as exceeding its limit.

Keep explicit reserveTokens and thresholdRatio overrides intact, and add regression coverage for metric-based token estimation, fallback estimation, and output-token-aware trigger budgeting.

* new target tokens and trigger tokens value

* fixes

* use imports and add unit test

* resolveMaxInputTokens

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-27 16:04:56 -07:00
Saoud Rizwan 8f6dae0ac0 fix(vscode): sync auto-approve task settings (#11929) 2026-06-27 15:13:14 -07:00
Saoud Rizwan 263b58f8c3 fix(llms): preserve fetch error cause details (#11928) 2026-06-27 14:51:56 -07:00
Saoud Rizwan b193a81ac9 fix: prevent API key field clearing on settings load (#11925)
* fix: prevent API key field clearing on settings load

* fix: cancel API key save on mask hydration
2026-06-27 14:32:14 -07:00
John Choi 92806c60ca fix(agents): derive messageModelInfo in the provider/model runtime path (#11903)
The standalone AgentRuntime({ providerId, modelId }) constructor built the gateway model in resolveRuntimeConfig and returned { ...rest, model } without deriving messageModelInfo. The prebuilt-model path preserves it, and core sessions populate it via buildMessageModelInfo, but standalone SDK callers lost it -- so assistant-message modelInfo and model-tagged telemetry (reasoning tokens, action-follow-through) emitted without provider/model dimensions.

Derive messageModelInfo as { id: modelId, provider: providerId } in that path (family omitted; it is optional and unavailable here). An explicit caller-provided messageModelInfo still wins. Adds provider-form tests covering both the derived and explicit-override cases.
2026-06-26 18:23:54 -07:00
Saoud Rizwan 3a05171e30 fix(sdk): preserve failed run error messages (#11904) 2026-06-26 18:04:53 -07:00
Saoud Rizwan a6e315a4a6 chore(cli): release v3.0.31 2026-06-26 17:37:36 -07:00
Saoud Rizwan 2714f93b45 chore(sdk): drop volatile catalog refresh from v0.0.53
The bun run version catalog regen dropped the xiaomi mimo-v2-omni,
mimo-v2-pro, and mimo-v2-flash models from the live data. mimo-v2-omni
is the xiaomi provider's defaultModelId in builtins.ts, so shipping the
refreshed catalog would point the default at a missing model and broke
the provider-ids test. Revert the catalog to the pre-release state and
ship v0.0.53 as a pure SDK code release; the catalog will refresh in a
later release once upstream data is stable.
2026-06-26 17:17:00 -07:00
Saoud Rizwan 3abeb8a90b chore(sdk): release v0.0.53 2026-06-26 17:03:04 -07:00
Tomás Barreiro 7830472017 Add open subscription page option to the ClinePass options (#11896)
* Add open subscription page option to the ClinePass options

* Address comments
2026-06-27 01:07:11 +02:00
Bee 83339c3c5a refactor: extension to use sdk provider list (#11888)
* refactor(vscode): use string type for api provider in proto

Replace the ApiProvider proto enum with plain string fields across\nmodels.proto and state.proto, and drop the enum<->string conversion\nmappings in api-configuration-conversion.ts. Updates ApiOptions and\nOpenAICompatible settings components accordingly.

* fix custom provider render

* format

* id

* remove extension providers file

* uses includes
2026-06-26 16:05:55 -07:00
Tomás Barreiro 664daf6ded Show cost has been covered by the users subscription (#11889)
* Show cost has been covered by the users subscription

* fix tests

* Update apps/cli/src/tui/components/status-bar.tsx

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

* Update wording

* Update tests

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-06-26 23:34:06 +02:00
Tomás Barreiro 5b1f8850af Update coupon code (#11890) 2026-06-26 23:33:11 +02:00
Tomás Barreiro c49d4121a3 Fix SDK tests (#11895) 2026-06-26 14:20:58 -07:00
Tomás Barreiro 7f495a5e99 Open ClinePass subscribe page (#11891) 2026-06-26 22:54:00 +02:00
Max 1e88a708bd upate changelog (#11886)
* upate changelog

* Update CHANGELOG.md

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

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-26 12:50:52 -07:00
Saoud Rizwan 408be18be9 fix(ci): harden ext-vscode stable release workflow (#11887)
* fix(ci): harden ext-vscode stable release workflow

- Resolve previous tag to the latest vX.Y.Z ancestor instead of the most
  recent reachable tag. The nightly workflow now pushes a nightly-main-*
  annotated tag on every main commit, so git describe was resolving the
  release notes' Full Changelog compare link to a nightly tag rather than
  the prior release tag.
- Extract the changelog section by exact version heading (and fail if it
  is missing) instead of always taking the first ## [ block, so a stale
  top entry can no longer ship as the release notes for a different
  version.
- Add a pre-publish Verify Changelog Entry gate so a release cannot be
  published unless CHANGELOG.md leads with the version being released.
- Add a Verify Marketplace Tokens gate so a missing VSCE_PAT/OVSX_PAT
  fails fast before packaging rather than mid-publish.
- In existing-tag mode, require the tag to point at the tested SHA so the
  published artifact always matches what CI verified.
- Add a concurrency group keyed on the tag to prevent duplicate
  concurrent publishes of the same release.

* fix(ci): validate stable release metadata before publish
2026-06-26 12:50:28 -07:00
Saoud Rizwan 5a9c637e32 fix(core): cap MCP tool names at 64 chars for OpenAI-compatible providers (#11885) 2026-06-26 12:42:57 -07:00
Saoud Rizwan 8152572641 fix(vscode): disable MCP marketplace tab from remote config (#11883) 2026-06-26 11:48:07 -07:00
Saoud Rizwan 0736e12e32 fix(vscode): refresh MCP hub after marketplace install (#11882) 2026-06-26 11:41:34 -07:00
Saoud Rizwan 690f80523f fix(vscode): preserve migrated OpenAI-compatible settings (#11880)
* fix(vscode): preserve migrated OpenAI-compatible settings

* test(vscode): cover OpenAI-compatible plan act selections
2026-06-26 10:56:50 -07:00
Saoud Rizwan 4fa1b8a291 fix(vscode): reject approvals from composer feedback (#11874) 2026-06-26 04:01:10 -07:00
Saoud Rizwan ed685b28e0 feat(vscode): allow cancelling queued prompts (#11875) 2026-06-26 03:58:46 -07:00
Saoud Rizwan 38338310d7 fix(vscode): timeout terminal cwd setup (#11871)
* fix(vscode): timeout terminal cwd setup

* refactor(vscode): simplify terminal cwd timeout
2026-06-26 03:49:36 -07:00
Saoud Rizwan 1c4a7885e6 fix(vscode): keep command output pinned to bottom (#11873) 2026-06-26 03:49:05 -07:00
Saoud Rizwan a0517db2fa feat: add shared marketplace uninstall support (#11870)
* feat: add shared marketplace uninstall support

* fix: avoid regex backtracking in marketplace skill sanitization

* fix: address marketplace uninstall review feedback

* fix: clean up remaining marketplace skill installs

* fix: remove marketplace skills from all agents

* fix: keep customize primitive tabs horizontal
2026-06-26 03:47:53 -07:00
Saoud Rizwan 38134ef967 fix(vscode): surface plugin bundled skills (#11868)
* fix(vscode): surface plugin bundled skills

* fix(core): align plugin skill settings lookup
2026-06-25 21:31:45 -07:00
Saoud Rizwan 8715cafce7 fix(vscode): rename user message reset actions (#11869) 2026-06-25 21:30:46 -07:00
Saoud Rizwan 46ee8ea329 feat(vscode): add customize section tabs (#11867)
* feat(vscode): add customize section tabs

* fix(vscode): reset customize section tab

* fix(vscode): reset customize section on initial type
2026-06-25 21:29:30 -07:00
Tomás Barreiro b7d9ea4500 Add a prompt to change to ClinePass when out of credits (#11866)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-25 21:11:23 -07:00
Saoud Rizwan 5147abc75e fix: hide workflows from customize menu (#11864) 2026-06-25 20:57:10 -07:00
Saoud Rizwan 9abd7ae8c3 fix(vscode): disable command auto-approval by default (#11865) 2026-06-25 20:56:30 -07:00
Saoud Rizwan bf83303bb7 fix(cli): require quoted prompts for one-shot mode (#11861) 2026-06-25 20:54:13 -07:00
Saoud Rizwan bfe5bf841a refactor: share marketplace install logic through core (#11862)
* refactor: share marketplace install logic through core

* fix: harden shared install helpers

* refactor: share mcp marketplace arg parsing
2026-06-25 20:34:42 -07:00
Tomás Barreiro eb362df3ba List ClinePass features in the CLI not-subscribed message (#11846)
* List ClinePass features in the CLI not-subscribed message

* fix tests
2026-06-26 05:22:15 +02:00
Saoud Rizwan f735ddcb7a fix(vscode): handle escape while editing user messages (#11860) 2026-06-25 20:20:33 -07:00
Saoud Rizwan 5b63d3e9c8 fix(vscode): preserve raw structured terminal commands (#11857) 2026-06-25 20:19:40 -07:00
Saoud Rizwan 1ff6a54825 fix: wrap customize tabs on narrow screens (#11855) 2026-06-25 19:17:49 -07:00
Saoud Rizwan b1a3cb6cfc chore(cli): release v3.0.30 2026-06-25 18:03:44 -07:00
Saoud Rizwan 78f1736723 test(cli): widen help terminal so long flag descriptions don't wrap
The --thinking description added in #11656 is long enough that at 120
columns commander wraps it, splitting "omitted leaves provider default"
across two lines. The TUI e2e assertion uses a contiguous getByText, so it
failed on the ubuntu-only TUI test leg, blocking the SDK publish gate.
Widen the help terminal to 200 columns so long descriptions render on a
single line.
2026-06-25 17:51:56 -07:00
Tran Binh Minh 28a014c1c6 docs: fix outdated skills enable path (#11838)
The Skills note pointed users to "Settings → Features → Enable Skills,"
but that toggle no longer exists — the Features settings section has no
Skills entry and skills are loaded by default. Point users to the actual
Skills menu (scale icon → Skills tab), consistent with the access path
already documented later in the same page.

Fixes #11740

Co-authored-by: Minhkunn <minh.12072k6@gmail.com>
2026-06-25 17:32:09 -07:00
Saoud Rizwan fed291e37a chore(sdk): release v0.0.52 2026-06-25 17:27:03 -07:00
Saoud Rizwan bb68351123 fix(vscode): avoid duplicate followup answer bubbles 2026-06-25 17:18:09 -07:00
Saoud Rizwan 84cb15813a fix(vscode): polish queued prompt panel 2026-06-25 17:17:48 -07:00
Saoud Rizwan c3671de7de fix(vscode): disable subagents (#11847) 2026-06-25 17:02:25 -07:00
Saoud Rizwan 26f737913f fix(vscode): show direct user messages immediately (#11845)
* fix(vscode): show direct user messages immediately

* fix(vscode): address pending chat bubble review
2026-06-25 16:53:15 -07:00
Dominic Cooney 50797dd82d vscode(ENG-2203): Migrate legacy MCP files, formats to shared settings file. (#11818)
* Migrate legacy MCP files, formats to shared MCP settings.
2026-06-26 08:23:02 +09:00
Saoud Rizwan 923ee3e137 fix(vscode): enable auto-approval defaults (#11840)
* fix(vscode): enable auto-approval defaults

* test(vscode): update file edit e2e for auto-approval defaults
2026-06-25 15:03:09 -07:00
Saoud Rizwan bbf5bb2302 fix(vscode): hide auto-approve notifications toggle (#11843) 2026-06-25 15:01:46 -07:00
Saoud Rizwan 0220b9a506 fix(vscode): queue chat submits during active turns (#11839)
* fix(core): avoid requeueing terminal failed prompts

* fix(vscode): avoid duplicate queued follow-up messages

* fix(vscode): keep approvals pending for queued messages

* fix(vscode): queue chat submits during active turns

* fix(core): restore pending prompt requeue on send failure

* test(core): remove pending prompt status churn
2026-06-25 15:01:10 -07:00
Ara 1b573275f8 fix(vscode): remove test server (#11842) 2026-06-25 14:39:59 -07:00
Robin Newhouse d7d74e0b89 fix(llms): preserve OpenRouter reasoning disable semantics (#11656)
* fix(llms): preserve OpenRouter reasoning disable semantics

* fix(llms): clarify reasoning token usage

* fix(llms): use OpenRouter reasoning effort none

* refactor(cli): extract reasoning resolution helper

* fix(cli): clarify thinking defaults

* feat(agents): capture unexpected reasoning token telemetry

* fix(cli): preserve reasoning on model change

* refactor(llms): table-drive reasoning token extraction

* fix(sdk): catalog unexpected reasoning telemetry
2026-06-25 14:35:05 -07:00
Saoud Rizwan bebc581652 fix(core): add non-interactive command guidance (#11815)
* fix(core): add non-interactive command guidance

* fix(core): refine non-interactive command guidance
2026-06-25 12:34:32 -07:00
Saoud Rizwan 21ebee3638 fix(sdk): keep SAP model filtering in clients (#11837) 2026-06-25 12:31:59 -07:00
Saoud Rizwan 7d78c5f074 refactor(vscode): attach checkpoints to user edits (#11832)
* refactor(vscode): attach checkpoints to user edits

* fix(vscode): improve checkpoint edit controls

* fix(vscode): align checkpoint edit actions

* fix(vscode): gate checkpoint restore edits
2026-06-25 12:21:17 -07:00
Saoud Rizwan 352a23a6da fix: stabilize SAP AI Core provider setup (#11833)
* fix: Filter SAP AI Core models based on mode-availibility

* chore: fix model picker test

* fix: harden SAP AI Core model filtering

* fix: pin SAP Cloud SDK to 4.6.0

* fix(vscode): simplify SAP AI Core model filtering

---------

Co-authored-by: David Knaack <david.knaack@sap.com>
2026-06-25 12:13:53 -07:00
Saoud Rizwan 176662ee20 fix(vscode): show pending state before queued prompt appears (#11836)
* fix(vscode): show pending state for chat sends

* fix(vscode): keep chat input editable during pending sends
2026-06-25 12:08:05 -07:00
Saoud Rizwan 309ad9da72 fix(vscode): show queued prompts while streaming (#11835)
* fix(vscode): show queued prompts while streaming

* fix(vscode): refine queued prompt updates
2026-06-25 11:54:44 -07:00
Saoud Rizwan 16a6926985 fix: improve ask option selection UI (#11824)
* fix: disable hover for selected ask options

* fix: mark ask options disabled after selection

* fix: hide duplicate ask option echoes
2026-06-25 11:51:43 -07:00
Saoud Rizwan d6db723c9d fix(vscode): keep command output pinned to bottom (#11825)
* fix(vscode): keep command output pinned to bottom

* fix(vscode): address command output scroll review
2026-06-25 11:31:54 -07:00
Tomás Barreiro ce1fc20a9f Limit the ClinePass CLI url to the CLI (#11811)
* Limit the ClinePass CLI url to the CLI

* fix tests

* Remove unused import

* Fix run-agent

* fix run-aent error message
2026-06-25 20:31:31 +02:00
Tomás Barreiro b780a5ec00 Fix ClinePass error mapping on VSCode (#11807)
* Fix ClinePass error mapping on VSCode

* refactor

* fix types

* remove unused constants

* refactor
2026-06-25 19:29:13 +02:00
Tomás Barreiro 285cd6d54c Fix vscode tests (#11831) 2026-06-25 10:16:39 -07:00
Tomás Barreiro ff845539e4 Fix createRequire (#11829) 2026-06-25 18:17:16 +02:00
Saoud Rizwan 05b0aa7dcb fix(vscode): flush state after model selection (#11822)
* fix(vscode): flush state after model selection

* fix(vscode): drain state after teardown cleanup
2026-06-25 04:13:40 -07:00
Saoud Rizwan 6cdd882acb fix(vscode): remove dead settings (#11819) 2026-06-25 03:38:47 -07:00
Saoud Rizwan d055b0941f feat(vscode): add marketplace (#11816)
* feat(vscode): add customize marketplace

* style(vscode): format marketplace imports

* fix(vscode): open marketplace mcp tab from configure

* fix(marketplace): redact authorization headers
2026-06-25 02:57:15 -07:00
Saoud Rizwan 21a0e4c4f1 fix(vscode): remove delay when sending message (#11817)
* fix(vscode): show new chat immediately on send

* fix(vscode): restore chat input after new task failure
2026-06-25 02:56:55 -07:00
Saoud Rizwan 54d022b536 fix(vscode): simplify auto-approve menu (#11814)
* fix(vscode): remove all-commands auto-approve option

* fix(vscode): clear legacy all-commands approval

* fix(vscode): remove external path auto-approve options

* Revert "fix(vscode): clear legacy all-commands approval"

This reverts commit 39af65f894.

* fix(vscode): ignore legacy all-commands approval

* fix(vscode): use all-commands auto-approve flag

* Revert "fix(vscode): use all-commands auto-approve flag"

This reverts commit 8b093ce654.
2026-06-25 02:46:07 -07:00
Dominic Cooney a1709d37e5 fix(vscode): make compact button run real SDK compaction (#11764)
* fix(vscode): make compact button run real SDK compaction

The compact button (and the typed /compact and /smol commands) sent the
literal text "/compact" to the model as a normal chat message. In the SDK
adapter only /workflow and /skill are expanded as runtime commands, so the
model received "/compact" as a prompt and improvised a fake "Conversation
Summary" without actually reducing the context window (CLINE-2503).

Wire the same SDK effect the CLI's /compact (alias /smol) uses:

- sdk-compaction.ts: compactSessionMessages(), the VSCode analog of the CLI's
  compactInteractiveMessages -- a manual-mode createContextCompactionPrepareTurn
  over the current transcript, force-enabling compaction and forwarding
  telemetry/sessionId.
- sdk-compaction-coordinator.ts: reads the active session transcript, runs the
  manual compaction, and restarts the session with the compacted messages via
  replaceActiveSession (same sequencing as a mode rebuild), preserving the
  session id and emitting a CLI-style status line. Guards no-session, mid-turn,
  and empty-transcript cases.
- SdkController.compactTask() exposes it; the condense slash handler now calls
  it instead of the no-op ask response.
- Webview: the compact-confirm button and typed /compact + /smol (with an active
  task) route to the condense RPC instead of sending literal text.

Adds unit tests for the helper, the coordinator, and the webview send routing.

* chore(vscode): drop trailing newline in condense handler (biome)

* test(vscode): cover manual compact flow

* test(vscode): use portable compact matcher

* test(vscode): assert compact calls without vitest matchers

* test(vscode): keep compact assertion type safe

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-25 02:28:07 -07:00
Saoud Rizwan b4157df509 fix(vscode): refresh Cline model info from catalog (#11806) 2026-06-25 02:20:22 -07:00
Saoud Rizwan 60dee73f0f fix(vscode): use SDK for mistake limit (#11808)
* fix(vscode): use SDK mistake-limit recovery

* test(vscode): cover mistake-limit stop response
2026-06-25 02:19:58 -07:00
Saoud Rizwan 2d66bd475e feat: add checkpoints (#11813)
* feat: add SDK-backed VS Code checkpoints

* fix: remove stale checkpoint view changes reset
2026-06-25 02:19:14 -07:00
Renee Huang a1374ae4a5 docs: add ClinePass subscription page and reorganize sidebar nav (#11672)
* docs: add ClinePass subscription page and reorganize sidebar nav

* docs: polish ClinePass copy and add cross-links

* more wording updates

* polishing

* docs: update 5x to 2-5x API rate limits

* add beta label to clinepass

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-24 22:03:39 -07:00
John Choi b8c62ecbae fix(onboarding): restore ClinePass models in onboarding (SDK parser dropped clinePass) (#11805)
* fix(onboarding): restore ClinePass models in onboarding

Root cause: the SDK's fetchClineRecommendedModels (@cline/core) silently
dropped the clinePass list. Its ClineRecommendedModelsData type and
normalizeResponse only handled recommended/free, so the recommended-models
endpoint's clinePass entries were stripped before reaching the extension.
Result: the onboarding ClinePass option appeared but the model list was always
empty ('No ClinePass models are available right now'), regardless of the
ext-cline-pass flag. This also affected any SDK consumer (CLI/JetBrains).

Also reverts the pre-login regression from #11798: that PR gated the first
onboarding screen on the extension-side clinePassEnabled flag, which is only
populated after login (featureFlagsService.poll runs on auth), so the ClinePass
option disappeared on the pre-login 'How will you use Cline?' screen.

Changes:
- @cline/core cline-recommended-models: parse/clone clinePass; include it in
  the type and offline fallback; treat clinePass-only responses as non-empty.
- OnboardingView: gate the ClinePass option on the webview useHasFeatureFlag
  (works pre-login) instead of the extension-side clinePassEnabled.
- Revert the extension-side clinePassEnabled plumbing added in #11798
  (FeatureFlagsService.getClinePassEnabled, state payload, ExtensionMessage,
  ExtensionStateContext default).

* test: add clinePass to recommended-models SDK mocks

ClineRecommendedModelsData now requires clinePass; update the mocked SDK
results in refreshClineRecommendedModels.test.ts so check-types passes.

* fix(onboarding): only offer ClinePass when models are available

Gate the ClinePass option on isClinePassEnabled AND models.clinePass.length > 0.
Previously, when the flag was on but the recommended-models request fell back
(or returned no clinePass entries), the option still appeared and routed users
into the ClinePass model step's empty state, where signup is disabled -- a dead
end instead of staying on Free/Frontier/BYOK.

* chore: trim ClinePass gate comment to one line
2026-06-24 17:41:14 -07:00
Bee 96b29d787e fix: normalize JSON-like tool inputs by schema (#11803) 2026-06-24 17:04:57 -07:00
Tomás Barreiro 5b96beb583 Link the CLI to the promo (#11794) 2026-06-25 01:48:09 +02:00
Dominic Cooney f2af0d700c refactor: simplify sdk terminal execution (#11789) 2026-06-25 08:43:41 +09:00
Tomás Barreiro a9c47f88fd Remove unused retry function (#11804) 2026-06-25 08:38:41 +09:00
John Choi 49884a1194 fix(sdk): batch outdated-read rewrites in MessageBuilder to preserve provider prefix caches (#11471)
* fix(sdk): batch outdated-read rewrites in MessageBuilder to preserve provider prefix caches

MessageBuilder previously rewrote stale read_files results to
'[outdated - see the latest file content]' eagerly on every re-read.
Each rewrite mutates bytes in the middle of the provider-facing
transcript, invalidating provider prefix caches (DeepSeek/Anthropic/
MiniMax-style) from that message to the end of the conversation. Agents
re-read files constantly (read -> edit -> verify), so long sessions paid
full uncached input price on most requests.

Now pending outdated rewrites accumulate and only commit once the total
reclaimable bytes cross a 64KB threshold, then apply as a single batch
(one cache break amortized over a large context saving). Committed
rewrites are sticky so subsequent requests stay byte-stable.

* fix(sdk): count only reclaimable locator bytes when batching outdated-read rewrites

Addresses review feedback (greptile P1, codex P2): pendingBytes was
incremented with the whole tool-result block size once per outdated
locator, so multi-file read_files results were overcounted (N stale
locators = N x block bytes), crossing the batch threshold far earlier
than intended and partially defeating the cache-stability guarantee.

Now estimateOutdatedReclaimBytes attributes bytes per stale entry in
the parsed read result (falling back to full text size only when the
whole block is outdated, matching replaceOutdatedReadContent), counted
once per block. Adds a multi-locator regression test where a 3-file
read result is invalidated file-by-file and must only commit when the
actual reclaimable bytes cross the threshold.

Real-session replay improved from 12.6% to 19.7% net-token reduction
with the accurate counting (commits defer longer, breaks amortize
better).

* fix(sdk): drop committed outdated rewrites when history is rolled back

Addresses review P1: committedOutdatedRewrites survived checkpoint
restore/clearHistory (the orchestrator reuses one MessageBuilder), so a
read that became the latest again after rollback stayed rewritten to
'[outdated...]' forever, hiding live file content from the provider.

Two guards: re-validate committed locators against the current index at
apply time, and clear the committed set in resetIndexes — that path only
fires on non-append-only history changes, where the provider prefix is
already broken, so stickiness loses nothing.

Adds a rollback regression test (commit rewrite, restore to before the
re-read, assert full content returns).

* test(sdk): trim redundant comments in rollback regression test

* fix(sdk): keep outdated-rewrite batching state across fresh message rebuilds

Addresses review feedback: the runtime provider path rebuilds Message
objects every request (agentMessagesToMessages constructs new literals),
so the identity-based reindex check fails each build and resetIndexes
fires. Clearing committedOutdatedRewrites there (added for the rollback
P1) recounted already-committed bytes as pending on every request — once
the first 64KB committed, every newly-stale small read rewrote
immediately, degenerating to eager behavior in steady state.

committedOutdatedRewrites now survives resetIndexes. Rollback
correctness is preserved without it: the apply-time re-validation is
identity-free, and commitOutdatedRewrites now prunes committed locators
that are no longer outdated in the current index plus entries whose
tool_use_id left the transcript. Both prunes are no-ops in append-only
growth since outdatedness is monotonic.

Adds two regression tests that route messages through the real
agent-message codec round-trip (fresh objects per build, as production):
steady-state deferral of a small newly-stale read after a committed
large one, and rollback restoring full content.

* fix(sdk): batch orphaned read results and count stale image bytes

Addresses robinnewhouse review (two pre-approval follow-ups):

1. Tool-name lookups went through toolNameByIdCache only, so a
   tool_result orphaned by compaction/rollback (paired tool_use gone)
   was invisible to the batching scan and pruned from committed state —
   reverting its rewrite mid-transcript in exactly the history-shrinking
   case the batching needs to survive. resolveToolName now falls back to
   tool_result.name at all three lookup sites (transform, reindex,
   commit scan).

2. estimateOutdatedReclaimBytes attributed only text/file entries, but
   replaceOutdatedReadContent also replaces stale image siblings
   (flagged by codex too). Image-heavy sessions accrued ~0 pending bytes
   and never crossed the threshold. The estimator now counts stale image
   payload bytes using the same positional marker counting as the
   rewriter (countOutdatedImageEntries).

Both regression tests fail before this change: orphaned result keeps
its committed rewrite through a codec round-trip, and a 4KB stale image
crosses a 2KB threshold that its ~70-byte text marker alone would not.

* perf(sdk): retune outdated-rewrite threshold to 128KB for executor caps

The 64KB default was calibrated before executor-layer output caps landed
(#11480/#11504: read_files/run_commands/search now cap at 48K chars).
With reads bounded at ~48K, 64KB sat awkwardly — one stale read can't
cross it, two overshoot — making it the worst non-extreme threshold in a
post-cap cost sweep.

Re-measured eager vs batched on 48K-capped transcripts (DeepSeek 10x
cache pricing): batching still beats eager 44-61%, confirming the
mechanism remains valuable after the caps (never-rewrite is now +35%
worse in long sessions). 128KB (~2-3 capped reads) is cheapest in both
short and long shapes, ~5-12 points better than the old 64KB.

Bumps LARGE_CONTENT test fixture to ~140KB so single-large-read commit
tests still exceed the raised threshold.

* fix(sdk): batch structured read tool results

* fix(sdk): resolve orphaned tool names for aggregate truncation

* test(sdk): trim redundant message builder cache tests

* style(sdk): trim message builder comments

* test(sdk): allow schedule history test more time on windows

* fix(sdk): preserve infinity outdated rewrite threshold

* perf(sdk): retune outdated rewrite threshold to 64KB

* Revert "test(sdk): allow schedule history test more time on windows"

This reverts commit ac21ef1702.

* test(sdk): fold message builder cache stability coverage

* fix(sdk): address stale read batching review
2026-06-24 16:17:34 -07:00
Dominic Cooney 49da86f60c chore: update biome vscode settings (#11788) 2026-06-25 08:16:50 +09:00
John Choi 84477dd84f fix(onboarding): gate ClinePass on reliable extension-side flag (#11798)
* fix(onboarding): show ClinePass models reliably + label the group ClinePass

Two issues:

1. Nightly feature-flag race. ClinePass was gated twice by two different flag
   clients: the recommended-models endpoint is server-gated by ext-cline-pass
   (PostHog-node), while the webview independently re-checked ext-cline-pass via
   PostHog-js to decide whether to show the option and keep the models. These
   reads race and disagree (mid auth/identify handshake, or when PostHog
   remote-config scripts are blocked by the webview CSP), so the ClinePass
   option could appear with an empty model list.

   Fix: make the server-gated payload the single source of truth. Onboarding
   shows the ClinePass option iff the payload contains ClinePass models
   (getUserTypeSelections now takes hasClinePassModels), and
   getRecommendedModelsData no longer re-filters response.clinePass on the
   webview flag. Removes the second racy webview PostHog read entirely.

2. Group label. The ClinePass group rendered as the raw provider id (CLINE-PASS).
   Render it with the product's proper casing (ClinePass). Model ids/names are
   intentionally left as-is (e.g. cline-pass/minimax-m3), since that's what the
   model is called.

* fix(onboarding): gate ClinePass on reliable extension-side flag

The ext-cline-pass flag is rolled out to internal cohorts only (QA/Cline
team/ClinePass Beta), not GA. Onboarding read it via the webview posthog-js
client, which is unreliable during onboarding (CSP blocks PostHog remote
config in Nightly, and it evaluates before auth/identify resolves) -- so
eligible team members saw ClinePass with an empty list / not at all.

Read the flag from the extension-side featureFlagsService instead (the same
server-evaluated source Settings/catalog already use), plumbed into webview
state like worktreesEnabled. Onboarding now shows ClinePass iff the flag is
enabled AND the payload contains ClinePass models, so the option and the
list are always in sync.

- FeatureFlagsService.getClinePassEnabled()
- getStateToPostToWebview: clinePassEnabled
- ExtensionState type + webview default
- OnboardingView gates on state.clinePassEnabled
2026-06-24 16:15:53 -07:00
Saoud Rizwan bd662d81f6 fix: bundle SAP AI Core provider auth (#11796)
* fix: bundle SAP AI Core provider auth

* fix: serialize SAP service-key auth calls
2026-06-24 13:45:00 -07:00
Tran Binh Minh d8a3086eaa docs(mcp): set type=streamableHttp in remote server example (#11670) (#11690)
The remote-server JSON example omitted the `type` field. Because the
config schema's z.union lists the SSE branch before streamableHttp
(intentionally, for backward compat), an untyped remote entry silently
resolves to the deprecated legacy SSE transport — the opposite of the
docs' own "Streamable HTTP (recommended)" guidance.

Add `"type": "streamableHttp"` to the example, rename the heading to
match, and add a sentence explaining that omitting `type` defaults to
legacy SSE.

Fixes #11670

Co-authored-by: Minhkunn <minh.12072k6@gmail.com>
2026-06-24 20:59:31 +02:00
Tomás Barreiro 8ecd136e52 Update the clinePass model list live (#11792)
* Generate the model list dynamically

* Do not return known models

* Make both calls in parallel

* Remove modelsDev catch on model generation

* readd error catching
2026-06-24 20:44:36 +02:00
Robin Newhouse 14a28b0559 fix(core): avoid nullable editor old_text schema (#11784) 2026-06-24 04:31:01 -07:00
Dominic Cooney c5378d1847 Merge pull request #11777 from cline/dpc/sdk-migration-simpler-login
SDK migration: move apps/vscode to bun + Cline SDK

### Description

This is the integration branch that moves the VSCode extension onto the Cline SDK and the bun toolchain. Major facts:

- **`apps/vscode` now runs on the Cline SDK.** The extension consumes `@cline/core`, `@cline/llms`, and `@cline/shared` through an adapter layer in `apps/vscode/src/sdk/` (single codepath — no `CLINE_SDK` flag). The webview still talks gRPC; the adapter translates between the gRPC handlers and SDK calls.
- **`apps/vscode` is folded into the root bun workspace.** Package management and task running move from npm/node to **bun**; the extension links the local `@cline/*` packages via `workspace:*` instead of pinned published versions. **Node remains the runtime** (extension host, standalone `cline-core`, esbuild `platform: node`, prebuild ABI targets).
- **npm lockfiles deleted; root `bun.lock` is authoritative** (`apps/vscode`, `webview-ui`, and `testing-platform` per-package lockfiles removed).
- **CI updated** for the new layout: the `ext-vscode-*` workflows install once at the root with bun and build the SDK before the extension build.
- **VSCode extension version bumped to `4.0.0`.**

### Test Procedure

Validated locally before opening:

- `bun run lint` — clean.
- Typechecks across SDK packages, `@cline/cli`, `@cline/cline-hub`, plus `apps/vscode` extension + webview `tsc` — all clean.
- Extension esbuild bundle and both webviews (`apps/vscode/webview-ui`, `apps/cline-hub`) build.
- Unit suites: `apps/vscode` bun-unit (932 pass), webview-ui vitest (247 pass), and SDK package suites (llms 323, agents 41, shared 202) pass.

Watching CI here for the authoritative cross-platform signal.

### Type of Change

-   [x]  New feature (non-breaking change which adds functionality)
-   [x] ♻️ Refactor Changes
-   [x] 🏃 Workflow Changes

### Pre-flight Checklist

-   [ ] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
-   [x] Tests are passing (`bun test`) and code is formatted and linted (`bun run format && bun run lint`)
-   [x] I have reviewed contributor guidelines
2026-06-24 15:46:39 +09:00
Dominic Cooney b5388e5c8d chore(vscode): bump version to 4.0.0 2026-06-24 14:48:03 +09:00
Saoud Rizwan 65b3977bc5 fix(vscode): forward OCA reasoning effort to SDK sessions (#11739) 2026-06-24 14:43:39 +09:00
Dominic Cooney f4a46cf8e7 fix(deps): drop global vite override so cline-hub webview keeps vite 8
The bun migration relocated apps/vscode/webview-ui overrides to the root
package.json, including vite ^7.1.11. As a workspace-wide override this
forced vite 7 onto apps/cline-hub/src/webview, which targets vite 8 and
uses rolldownOptions in its vite.config.ts. That broke `bun run -F
@cline/cli build` (cline-hub build:webview) with TS2769 on rolldownOptions.

Removing the global override lets each workspace resolve its declared
vite: webview-ui stays on vite 7.3.5, cline-hub resolves vite 8.0.16.
Both webviews build and the webview-ui vitest suite (247 tests) passes.
2026-06-24 14:36:34 +09:00
Dominic Cooney 0b47033a0f fix(core): suppress cross-package import lint in SAP handler-factory test 2026-06-24 14:23:23 +09:00
Saoud Rizwan c94ef4b750 fix(vscode): revert OpenAI-compatible metadata limit plumbing (#11775)
* fix(vscode): stop deriving output limits from model metadata

* fix(vscode): send OpenAI-compatible output token limit (#11776)
2026-06-24 14:12:00 +09:00
Tomás Barreiro 3e855f7d3f Fix other instances of issues with the litellm model list (#11773)
* Fix other instances of issues with the litellm model list

* address comment
2026-06-24 14:12:00 +09:00
Saoud Rizwan 2cd062ce68 fix(vscode): keep model metadata out of provider settings (#11772)
* fix(vscode): keep model metadata out of provider settings

* fix(vscode): prune stale provider model metadata

* docs(vscode): explain provider metadata pruning
2026-06-24 14:12:00 +09:00
Tomás Barreiro be6999209e Prevent injecting other models into the LiteLLM model list (#11771) 2026-06-24 14:12:00 +09:00
Saoud Rizwan 9a1e6121c7 fix(llms): align SAP AI Core provider config (#11759)
* fix(core): align SAP AI Core mode config

* fix(llms): map SAP AI Core credentials to service binding
2026-06-24 14:12:00 +09:00
Tomás Barreiro 4ae0ff4d5a Build the SDK sourcemaps (#11757)
* Build the SDK sourcemaps

* Do not minify

* do not minify packages when building sourcemaps
2026-06-24 14:12:00 +09:00
BarreiroT 18737f1448 Map ClinePass model information 2026-06-24 14:12:00 +09:00
Saoud Rizwan b56ce72fc7 fix(core): forward SAP provider options to gateway (#11756) 2026-06-24 14:11:59 +09:00
Max 425182c7c0 Remove chat scroll action button (#11734)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-06-24 14:11:59 +09:00
Max 30fe302a71 fix retry after cline login issue (#11646)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-06-24 14:11:59 +09:00
BarreiroT eadbc09ffd Update generated models 2026-06-24 14:11:59 +09:00
BarreiroT 3178487bdd fix cline-pass options 2026-06-24 14:11:59 +09:00
Saoud Rizwan f58941c500 fix(core): add OCA legacy reasoning effort (#11746) 2026-06-24 14:11:59 +09:00
Saoud Rizwan 8da5ffa874 fix: wire up SAP provider (#11745)
* fix(vscode): wire SAP AI Core session config

* fix(vscode): remove redundant SAP base URL mapping
2026-06-24 14:11:59 +09:00
Dominic Cooney 9ed95d1dc2 fix(llms): restore provider-request capture wiring lost in SDK migration 2026-06-24 14:11:59 +09:00
Dominic Cooney 5fc7341312 chore: regenerate bun.lock after rebase onto main 2026-06-24 14:11:37 +09:00
Dominic Cooney d5b38f38cb fix(vscode): preserve OrgClinePass error UI through SDK rebase 2026-06-24 14:11:37 +09:00
Tomás Barreiro 386401b41f Identfy accounts for feature flag resolution (#11741)
* Identify accounts for Feature Flag resolution

* simply code
2026-06-24 14:11:37 +09:00
Max 6471d2475a if search result is undefined then don't crash the extension (#11733)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-06-24 14:11:37 +09:00
Max Paulus 🥪 fada079558 remove gap between approve bar and input box 2026-06-24 14:11:37 +09:00
BarreiroT 31d50ff54a Add log 2026-06-24 14:11:37 +09:00
BarreiroT efebc6380b Fix imports 2026-06-24 14:11:36 +09:00
Saoud Rizwan 2acc25cf6e test(vscode): raise vitest testTimeout to 20s to fix import-cost flakes (#11729)
Several vitest suites lazily await import() their subject inside the first
test (so vi.mock factories apply first). That import pulls in heavy workspace
packages (@cline/core, @cline/llms, @cline/shared), and on loaded CI runners
the first test in a file intermittently exceeds the 5s default timeout and
fails the nightly (observed in catalog.test.ts, now resolveModelInfo.test.ts).
Set a global 20s testTimeout so import cost attributed to the first test does
not cause flakes.
2026-06-24 14:11:36 +09:00
Saoud Rizwan 1ebe99c676 test(vscode): add invalidateProviderListings to auth-service mock controllers (#11727)
#11720 (feature flag resolution on startup) added a
controller.invalidateProviderListings() call to AuthService.sendAuthStatusUpdate
but did not update the test's mock controllers, which only stubbed
postStateToWebview. The new call threw on the mocks, so the throw happened
before postStateToWebview ran (failing the 'polls feature flags' test) and
caused subscribeToAuthStatusUpdate to delete the handler in its catch block
(failing the 'removes subscription on cleanup' test). Add the now-required
invalidateProviderListings stub to the mock controllers.
2026-06-24 14:11:36 +09:00
BarreiroT f5bbbd7f08 Log feature flags 2026-06-24 14:11:36 +09:00
Saoud Rizwan 8e4a2b8a19 fix(sdk): repair exposed provider auth routing (#11721)
* fix(sdk): repair exposed provider auth routing

* fix(sdk): use accessible ZAI coding plan default

* fix(sdk): use live Poolside model default

* docs(vscode): explain SDK provider key fallback
2026-06-24 14:11:36 +09:00
Tomás Barreiro dfdeffb558 Fix ModelAutocomplete selection (#11718) 2026-06-24 14:11:36 +09:00
Tomás Barreiro 1d22a51e30 Fix Feature Flag resolution on startup (#11720)
* Fix Feature Flag resolution on startup

* remove irrelevant test
2026-06-24 14:11:36 +09:00
Saoud Rizwan c827537386 test(vscode): use toBe instead of toMatchObject in proto conversion test (#11712)
api-configuration-conversion.test.ts is picked up by both the vitest
runner and the mocha-based vscode-test integration runner (.vscode-test.mjs
globs src/shared/**/*.test.js). vitest's jest-compat matcher toMatchObject
does not exist in the mocha runtime, so the test passed under vitest but
threw "toMatchObject is not a function" in the integration suite, failing
the nightly publish. Assert the two provider fields with toBe, which works
under both runners.
2026-06-24 14:11:36 +09:00
Saoud Rizwan f73121e369 test(vscode): warm catalog import to fix flaky 5s timeout (#11711)
The first test in catalog.test.ts paid the cost of dynamically importing
./catalog (which pulls in @cline/core, @cline/llms and @cline/shared)
inside its own 5s test timeout, intermittently failing CI/nightly runs.
Warm the import once in beforeAll so the cost falls outside any per-test
clock.
2026-06-24 14:11:36 +09:00
Saoud Rizwan 52bbda183d feat(vscode): expose additional SDK providers (#11703)
* feat(vscode): expose additional SDK providers

* fix(vscode): reserve skipped provider enum slots

* fix(vscode): keep Z.AI Coding Plan provider-specific
2026-06-24 14:11:36 +09:00
Saoud Rizwan 80036cef44 fix(sdk): route LiteLLM model fetches through SDK (#11705)
* fix(vscode): improve LiteLLM model fetch errors

* fix(vscode): align LiteLLM fetch return contract

* fix(sdk): improve LiteLLM private model fetch

* chore(vscode): drop duplicate LiteLLM fetch changes

* fix(vscode): route LiteLLM refresh through SDK
2026-06-24 14:11:36 +09:00
Saoud Rizwan b37f872a1f fix(vscode): honor OpenAI-compatible model settings (#11710)
* fix(vscode): honor OpenAI-compatible model settings

* fix(vscode): simplify OpenAI-compatible model bridge

* fix(vscode): respect OpenAI-compatible image support
2026-06-24 14:11:35 +09:00
Max ebe2b6498b fix(vscode): use Codex OAuth credentials (#11691) 2026-06-24 14:11:35 +09:00
BarreiroT 365250c785 Fix tests 2026-06-24 14:11:35 +09:00
BarreiroT 1e208472da fix tests 2026-06-24 14:11:35 +09:00
Tomás Barreiro ee974ea863 Fix ClinePass auth (#11680)
* Return local providers with ClinePass in the new extension

* Fix import

* Fix ClinePass auth
2026-06-24 14:11:35 +09:00
Tomás Barreiro a6416d02c8 Return local providers with ClinePass in the new extension (#11678)
* Return local providers with ClinePass in the new extension

* Fix import
2026-06-24 14:11:35 +09:00
Max Paulus 🥪 55cfeaec65 fix broken CI tests 2026-06-24 14:11:35 +09:00
Max Paulus 🥪 acfe779b20 fix local build not picking up .env file 2026-06-24 14:11:35 +09:00
Max Paulus 🥪 cff8c22f06 update vscode ignore
vsix bundling failed because of some unignored files
2026-06-24 14:11:35 +09:00
Max Paulus 🥪 8f2be20858 fix broken integ tests 2026-06-24 14:11:35 +09:00
Max Paulus 🥪 f1fd189631 remove storage tests from vitest
- these run under node resolution so they can see "bun:test" imports.
- these tests will get run by scripts/run-bun-unit-tests.ts instead
2026-06-24 14:11:34 +09:00
Cline Agent 9b03cb878d fix: repair SDK ClinePass webview rebase
Restore the webview feature-flag hook needed by the ClinePass onboarding/settings UI, but implement it against the existing posthog singleton instead of posthog-js/react so tests do not pull in a second React copy.

Make ClinePass settings follow the SDK provider-catalog pattern: render the Cline account card, resolve models with useProviderModels("cline-pass"), and persist selections with useProviderConfig/useProviderModelSelection for providerId="cline-pass". Remove the stale origin/main props that tried to drive the SDK-era ClineModelPicker, which is intentionally Cline-provider specific.

ClinePass remains hidden by the ext-cline-pass flag in settings/onboarding, and its model info hides token usage costs because billing is subscription-based.
2026-06-24 14:11:34 +09:00
Cline Agent 3dc020185f fix: post-rebase ClinePass plumbing for SDK migration
Resolve type-check and test breakages from rebasing the ClinePass
feature (origin/main) onto the SDK migration branch:

- provider-keys: re-add cline-pass to ProviderKeyMap and
  NON_SDK_PROVIDER_DEFAULTS (removed by the 'remove unused code'
  commit which predated ClinePass), so getProviderModelIdKey and
  getProviderDefaultModelId handle the cline-pass provider.
- provider-id: register 'cline-pass' in KNOWN_API_PROVIDERS so the
  Record<ApiProvider, true> constraint is satisfied.
- refreshClineRecommendedModels: add optional 'clinePass' field to
  ClineRecommendedModelsData so the RPC handler can map it into the
  proto response without a type error.
- refreshClineRecommendedModelsRpc: guard models.clinePass with ?? []
  for the same reason.
- handleClinePassProviderSelection: pass undefined (not null) to
  accountService.switchAccount to match the SDK signature.
- provider-keys.test: remove a duplicate closing brace left by the
  conflict resolution.
- Biome formatting (asNeeded semicolons) applied by check-types.
2026-06-24 14:11:34 +09:00
Dominic Cooney 4829f08b3f fix(vscode): reliable MCP OAuth on the SDK extension (ENG-2108, CLINE-2304) (#11529)
* fix(vscode): store MCP OAuth in shared settings file like the CLI (ENG-2108)

VSCode stored MCP OAuth tokens in a single mcpOAuthSecrets secrets blob
keyed by sha256(name:url), while the CLI/SDK store per-server oauth state in
cline_mcp_settings.json. The two never interoperated (CLI auth was invisible to
VSCode), and VSCode's read-whole-blob/write-whole-blob through StateManager's
non-refreshing cache meant concurrent windows clobbered each other's tokens.

- Store MCP OAuth state in the shared settings file in @cline/core's format.
- Reads are fresh from disk; writes are scoped read-modify-write of one
  server's oauth key via updateMcpServerOAuthState (now atomic temp+rename).
- Replace the vscode:// callback flow with HTTP-based token collection via
  authorizeMcpServerOAuth (same local loopback flow the CLI uses).
- Reconnect an unauthenticated server when its tokens appear (e.g. CLI auth).
- One-time migration of legacy mcpOAuthSecrets tokens into the shared file.
- Remove McpOAuthRedirectResolver, mcpOAuthFlow, completeOAuth, and the
  mcp-auth URI callback route.

* feat(vscode): add --instances/--random-port to MCP OAuth test server

Lets you start several independent test servers, each on its own OS-assigned
random port, so you can add multiple streamableHttp MCP servers to Cline at
once and exercise concurrent OAuth flows. baseUrl now reflects the actually
bound port so discovery metadata and redirect URIs stay correct under random
ports.

* fix(vscode): stop MCP OAuth handshake writes from livelocking the settings watcher (ENG-2108)

Now that codeVerifier/clientInformation live in the shared settings file, the
MCP SDK's per-connect-attempt saveCodeVerifier() writes were tripping the
settings watcher, which re-entered updateServerConnections -> connectToServer
-> another write, looping forever. It was especially bad with two+
unauthenticated servers, where each server's verifier churn re-triggered the
other (visible as a flickering, ever-changing codeVerifier nonce).

The watcher now compares a connection-relevant fingerprint (full per-server
config minus the oauth block, plus a boolean for whether an access token
exists) and skips writes that only churn OAuth-handshake fields. A token
appearing/disappearing still changes the fingerprint, so CLI/other-window
authorization continues to trigger a reconnect via serverGainedOAuthTokens.

* feat(vscode): print paste-ready MCP settings fragment from OAuth test server

On startup the test server now emits an mcpServers JSON fragment (nested
transport shape, matching cline_mcp_settings.json) alongside the banner, so you
can paste it straight into the settings file instead of hand-writing it. With
--instances the entries get distinct names (oauth-test-1, ...), each carrying
its actual bound port.

* fix(vscode): atomic MCP settings writes + fingerprint gate; drop timer guards (CLINE-2097)

Deleting one MCP server could empty the whole list. Root cause: settings
writes were non-atomic (fs.writeFile), so chokidar (and any other process)
could read a transient empty/torn file mid-write and reconcile to zero servers.
The previous fix only masked this with a per-process isUpdatingClineSettings
boolean cleared on a 300ms timer — it did nothing for the CLI or other windows
and was racy.

Replace both timer guards (isUpdatingClineSettings, isUpdatingFromRemoteConfig)
with two deterministic, process-agnostic mechanisms:

- writeSettingsFile(): atomic temp-file + rename for every settings write, so
  any reader always sees a complete file. Holds for any number of concurrent
  writers (CLI, multiple windows, SDK OAuth handshake).
- content fingerprint: the watcher reconciles only when the connection-relevant
  view changed. writeSettingsFile pre-seeds the fingerprint so our own write is
  a no-op, while a genuine change from any other process is still processed.
  Because reconcile is idempotent and reads are never torn, a missed
  suppression is at worst a redundant reconnect, never data loss.

All RPC writers (toggle disabled, autoApprove x2, timeout, add, delete) and the
remote-config sync now go through writeSettingsFile. Removes all setTimeout(.,
300) flag juggling.

* feat(vscode): add a non-guessable 'frozzle' tool to the MCP OAuth test server

The MCP OAuth test server now serves tools/list + tools/call exposing a
'frozzle' tool whose output cannot be derived without calling it (reverse the
string and swap each letter's case, wrapped in guillemets). This gives an eval
a reliable end-to-end signal that the OAuth-authenticated MCP round-trip really
happened: a correct 'frozzle <text>' answer can't be hallucinated. The
transform is easy to verify at a glance and invertible. Adds frozzle.test.ts.

* fix(sdk): drop lingering OAuth callback sockets on close so deny->approve re-auth works (ENG-2108)

The local OAuth callback server's close() called Server.close(), which only
stops accepting new connections and lets existing keep-alive sockets linger.
The browser / global-fetch connection pool keeps such a socket to the fixed
callback port (1456) alive. So after the user denied an MCP OAuth request and
retried, the retry's approve callback could be delivered over the pooled socket
to the FIRST (already-settled) server. That server's settle() was a no-op, so
waitForCallback() never resolved, finishAuth()/token exchange never ran, and no
token was saved — the server stayed unauthenticated (the deny->approve repro).

Call server.closeAllConnections() in close() so no pooled socket outlives the
server. Adds a regression test driving a keep-alive agent across close().

* fix(vscode): actually reconnect MCP server when toggled back on (ENG-2108)

toggleServerDisabledRPC only flipped the in-memory disabled flag and set status
to 'connecting', but never rebuilt the connection. A disabled server's
connection has no live transport/client, so re-enabling left it stuck on the
yellow 'connecting' indicator forever and never re-advertised its tools to the
agent.

Tear down and rebuild the connection through deleteConnection + connectToServer
(which opens a real transport when enabled, or a disconnected stub when
disabled), then notifyWebviewOfServerChanges so the SDK session's tool list is
refreshed. OAuth state is preserved (deleteConnection doesn't clear it). Adds
McpHub.toggleServerDisabledRPC.test.ts.

* fix(vscode): reload MCP tools silently without chat spam (ENG-2108)

Restarting the SDK session to pick up MCP tool changes appended visible chat
messages ('MCP tools changed - reloading...' and 'MCP tools reloaded
successfully...') plus a completion_result banner. Toggling several servers
piled up many of these. Tool reloading should be transparent.

Emit only the session status transitions (running -> idle) via
emitSessionEvents([], ...) instead of appendAndEmit, so no chat messages or
completion banner are shown. Genuine reload failures still surface an error
message. Updates sdk-mcp-coordinator.test.ts accordingly.

* docs(mcp): clean up comments to describe current behavior

Revise comments across the MCP OAuth and settings code to document the code as
it stands, dropping references to prior implementations, task IDs, and
before/after narration. Also reflow the auth-server regression test to the
repository's formatter. No behavior change.

* fix(vscode): atomic fallback write in remote MCP sync; document sync OAuth I/O

Make the no-McpHub branch of syncRemoteMcpServersToSettings write via an
atomic temp-file + rename so a concurrent reader never observes a torn or
empty settings file, matching every other settings write.

Document why the OAuth state read-modify-write in McpOAuthManager is
synchronous: it serializes this process's shared-file updates without a
Promise queue, which we prefer over async I/O for reliability of the
cross-process settings file.

* fix(mcp): serialize settings read-modify-writes

* docs(vscode): clarify MCP settings create race

* fix(vscode): create MCP settings atomically

* fix(cli): keep clearing missing MCP OAuth state a no-op

* fix(vscode): avoid yielding while holding MCP settings lock (#11596)

* fix(mcp): async lock acquisition for VSCode MCP settings/OAuth writes

Add updateMcpSettingsFile/updateMcpServerOAuthStateAsync to @cline/core that
yield the event loop while acquiring the cross-process settings lock instead of
blocking it with Atomics.wait. The critical section stays synchronous and the
mutator stays pure, so the lock is never held across an await and serialization
is preserved without an in-process queue.

Route the VSCode extension host's OAuth state writes (McpOAuthManager) through
the async variant so a connection-time OAuth callback can no longer freeze the
extension host event loop or deadlock against an in-flight updateMcpSettingsFile
whose lock-releasing continuation needs the loop.

Unify the sync and async acquisition paths on a shared reentrancy guard
(activeLocks) so a nested settings update on the same file fails fast instead of
self-deadlocking.

Tests: contended async serialization asserting zero Atomics.wait calls, async
stale-lock reclaim, reentrancy fail-fast, and uncontended run+release.

* fix(mcp): bootstrap missing settings file inside the lock; tidy docs

Creating the MCP settings file now happens in one place: the locked
read-modify-write helpers. A missing file reads as an empty settings object, so
the first write to a fresh path (e.g. a fresh-install `cline mcp add`) creates
it inside the lock instead of throwing ENOENT. The SDK (updateMcpSettingsFile /
updateMcpSettingsFileSync) and the VSCode lock helper share this contract, so
callers no longer need to pre-create the file. Add regression tests for the
SDK, the CLI wizard addServer(), and the VSCode helper on a missing path.

Also flag the synchronous SDK entry points (updateMcpSettingsFileSync,
updateMcpServerOAuthState) as preferring their async siblings, with a TODO to
delete them once all callers migrate, and tighten the lock-helper doc comments
to describe current behavior.

* fix(vscode): finish npm->bun migration in dev tooling, tasks, and docs

The npm->bun migration (#11632) updated package scripts, .vscodeignore and .vscode-test.mjs but left a trail of npm/npx/node invocations in editor configs, dev scripts, and docs. Following the breadcrumbs from 'npm run protos':

- .vscode/launch.json: standalone-core debug uses 'bun <file>.ts' (was npx tsx); Open Storybook uses 'bun run' (was npm run).
- .vscode/tasks.json: all task commands use 'bun run' (was npm run).
- scripts/run-extension-host.sh and .claude/hooks/claude-code-for-web-setup.sh: 'bun run' (was npm run).
- debug-harness/server.ts: shebang 'bun'; build steps use 'bun run protos', 'bun esbuild.mjs', 'bunx vite build' (were npm/node/npx).
- dev script shebangs (test-hostbridge-server, test-standalone-core-api-server, testing-platform-orchestrator, interactive-playwright): '#!/usr/bin/env bun' (was npx tsx).
- WebviewProvider HMR hint, e2e README, copilot-instructions, PR template, mcp-oauth-test-server docs, generate-state-proto message, tsconfig.test comment, state-keys test comment: bun.

Left untouched (correct per .clinerules/bun-and-node): Node-runtime invocations (node build.mjs), prebuild-install --target=<node>, vsce, 'npm install -g cline' (user CLI install), and App.stories.tsx mock chat fixtures.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-06-24 14:11:34 +09:00
Dominic Cooney 82d1846a45 Migrate apps/vscode from npm/node to bun (#11632)
* chore(vscode): migrate package management & build from npm/node to bun

Fold apps/vscode (+ webview-ui, testing-platform) into the root bun
workspace so the extension consumes the local @cline/* SDK packages via
workspace symlinks instead of pinned published versions, eliminating the
SDK vendoring cycle. Node remains the runtime (extension host, standalone
cline-core, esbuild platform:node, prebuild-install ABI target).

- root: drop "!apps/vscode", add nested members, relocate overrides to
  root, add trustedDependencies [better-sqlite3, grpc-tools]
- apps/vscode: @cline/* -> workspace:*, scripts -> bun/bunx,
  npm-run-all -> bun --parallel, drop cross-env; keep esbuild + vite;
  declare previously-hoisted phantom deps (nice-grpc-common, playwright)
- package-standalone.mjs: npm install -> bun install (isolated dist dir)
- CI: setup-bun + single root bun install --frozen-lockfile, build:sdk
  before extension build, better-sqlite3 binary + zero-test guards;
  publish workflows intentionally keep setup-node for vsce/ovsx
- docs/comments: curated pass (keep-list vs rewrite-list), add
  apps/vscode/docs/bun-migration-notes.md guard doc
- delete npm lockfiles (root bun.lock authoritative)

Deferred to follow-up PRs: test-runner migration to bun test (Phase 4)
and devDep cleanup (Phase 6).

* test(vscode): add bun test foundation for the vitest-native unit suites

Phase 4a of the test-runner migration. Adds a bun test runner that
reaches full parity (582 pass / 0 fail / 50 files) with the existing
vitest SDK-adapter + model-catalog suite, without touching the
@vscode/test-cli integration tests or the webview vitest suite.

- bunfig.toml: [test] preload
- src/test/bun-test-preload.ts: mock.module() shadows `vscode` and
  `@cline/core` with their unit-test stubs (bun's onResolve plugin hook
  does not intercept host/symlinked specifiers); seeds real @cline/core
  export names as undefined to satisfy bun's strict ESM named-import
  linking; full vitest->bun:test shim (vi.fn/mocked/spyOn, describe/it/
  expect/before*/after*)
- scripts/run-bun-tests.ts: mirrors vitest.config.ts include[] exactly and
  runs with --parallel for per-file mock isolation (bun test's single-process
  default lets mock.module clobber across files)
- test:bun script

* test(vscode): migrate node-side unit suite from mocha to bun test

Phase 4b of the test-runner migration. The standalone mocha unit runner
(.mocharc spec: __tests__/* + test/services/**) was already broken under
bun (mocha was a phantom dependency — only @types/mocha/ts-node were
declared, npm hoisted mocha transitively). Migrate it to `bun test`.

- codemod 77 files: import { ... } from "mocha" -> "bun:test", renaming
  before->beforeAll / after->afterAll at imports and call-sites; chai,
  should and sinon kept as libraries (they work under bun test)
- convert sinon.stub() on ESM namespace exports to mock.module()/spyOn
  (bun loads real ESM: "ES Modules cannot be stubbed")
- scripts/run-bun-unit-tests.ts: runs the .mocharc spec set with one
  isolated `bun test` process per file (Bun.spawn + concurrency pool),
  restoring vitest-forks module-registry isolation (bun's single-process
  default lets mock.module leak across files)
- scripts/codemod-mocha-{to-bun,this}.ts: one-shot migration tooling
- test:unit now runs the bun unit runner; CI calls bun + a non-zero
  pass-count guard instead of `bunx nyc ... mocha`
- tsconfig: add root node_modules/@types to typeRoots so `bun:test`
  types resolve under tsc; cast loose os.userInfo mocks in shell.test

Result: unit suite 58 files / 880 pass / 0 fail; vitest set still
582/0. @vscode/test-cli integration tests and webview vitest unchanged.

* chore(vscode): remove dead mocha-runner deps and artifacts

Phase 6 cleanup after the bun test migration. The standalone mocha unit
runner is gone (replaced by scripts/run-bun-unit-tests.ts), so its
config and now-unused devDependencies are removed.

- remove dead files: .mocharc.json, tsconfig.unit-test.json,
  src/test/requires.ts, .nycrc.unit.json
- remove unused devDeps: @types/mocha, @types/proxyquire, ts-node,
  tsconfig-paths, cross-env, npm-run-all, nyc, proxyquire, husky
  (root owns the husky hook; chai/should/sinon stay — used as libs)
- install:all -> single root `bun install` (workspace covers webview-ui)
- drop .mocharc.json / .nycrc*.json from CI paths-filters and
  .vscodeignore; add bunfig.toml to the filters

Verified: check-types clean, unit 880/0, vitest 582/0.

* fix(vscode): import bun:test globals in tests that relied on ambient @types/mocha

CI Quality Checks (clean `bun install` without @types/mocha) surfaced
TS2582/TS2304 "Cannot find name 'describe'/'it'/'beforeEach'" in test
files that used the global mocha/jest test functions without importing
them. The Phase 4b codemod only rewrote files that imported from
"mocha"; these used ambient globals, so they were missed (and passed
locally because a stale @types/mocha lingered in node_modules).

Add explicit `bun:test` imports (before->beforeAll, after->afterAll in
TelemetryService.test.ts). chai/sinon stay as libraries.

Verified against a clean tree (no @types/mocha): check-types 0 errors,
unit suite 58 files / 880 pass / 0 fail.

* style(vscode): biome-format migrated test files + codemod scripts

The mocha->bun:test codemod and manual import edits left formatting that
didn't match biome (the CI `format` check, which validates files changed
since main, flagged them). Also narrow setup.ts's bun:test import to the
actually-used beforeEach/afterEach (describe/it only appear in a JSDoc
example), fixing a noUnusedImports lint error.

ci:check-all (check-types + lint + format) now passes locally.

* fix(webview-ui): declare phantom deps + pin React 18 types under bun workspace

Folding webview-ui into the bun workspace changed its install topology
from an isolated npm flat tree to the shared hoisted store, surfacing
two classes of pre-existing latent issues that npm hoisting had masked:

1. Phantom dependencies: src imports `marked`, `unist`, `unist-util-visit`
   and `@heroui/theme` directly but never declared them. Declared them
   (marked ^15, unist-util-visit ^5, @types/unist ^3, @heroui/theme 2.4.26).
2. React types: @testing-library/react's optional peer pulls @types/react@19
   into a resolvable location; tsc mixed it with the toolkit's React 18
   types (React 19 dropped Component.refs), breaking 452 JSX usages. Pin
   react/react-dom type resolution to webview-ui's React 18 copy via
   tsconfig paths.

build:webview (tsc -b && vite build) and ci:check-all now pass.

* fix(vscode): restore @types/mocha for integration build + add bun:test types

The @vscode/test-cli integration runner still uses mocha, and
tsconfig.test.json compiles all src/**/*.test.ts (including bun-migrated
files) to out/. So:
- restore @types/mocha (integration compile needs the mocha ambient types)
- add `bun` to tsconfig.test.json types + root @types to both tsconfig
  typeRoots so `bun:test` resolves under tsc for the migrated tests

* fix(vscode): declare glob — phantom dep used by package-standalone.mjs

scripts/package-standalone.mjs imports `glob` but it was never declared
(resolved transitively under npm's flat hoist). Under the bun workspace
store it's unresolvable, failing postcompile-standalone with
ERR_MODULE_NOT_FOUND. Declare glob ^11 (modern named-export API).

compile-standalone now produces dist-standalone/standalone.zip.

* fix(ci): strip ANSI before vitest zero-test guard grep

The vitest summary line colorizes the count ("Tests  <ansi>582 passed"),
so the count isn't adjacent to the "Tests" label in raw bytes and the
guard regex failed even though 582 tests passed. Strip ANSI escapes
before matching.

* fix(vscode): declare minimist — phantom dep in testing-platform-orchestrator

scripts/testing-platform-orchestrator.ts imports `minimist` (undeclared,
resolved transitively under npm hoist). Declare it so the testing-platform
integration job runs under the bun workspace store.

* fix(vscode): restore tsconfig-paths for integration runner; tp-orchestrator uses bun

Phase 6 over-removed tsconfig-paths: test-setup.js (loaded by the
@vscode/test-cli mocha integration runner) requires it to resolve @/
aliases in the compiled out/ tree — the extension host test runner failed
with "Cannot find module 'tsconfig-paths'". Restore it. Also switch the
testing-platform spawn from `npx ts-node index.ts` to `bun index.ts`
(bun runs TS natively; avoids the removed ts-node).

* fix(vscode): route tests by bun:test import marker; integration runner stays mocha

The mocha->bun codemod swept up tests that the Node-based @vscode/test-cli
integration runner compiles/runs, which cannot load the `bun:test` builtin
(and some need the real VSCode host). Establish a single source of truth:
a *.test.ts is bun-runner-owned IFF it imports "bun:test".

- run-bun-unit-tests.ts: discover files by the bun:test import marker
  (not fixed globs), so every migrated file runs under bun.
- build-tests.js: generate a tsconfig that excludes all bun:test files
  from the integration compile (json5-parsed), so out/ never contains
  bun:test; gitignore the generated config.
- .vscode-test.mjs: exclude the bun unit dirs from the runner globs.
- revert host-dependent tests (hostbridge/*, extension, terminal,
  FileContextTracker host bits) and 3 files with sinon-on-ESM/behavioral
  issues (ClineIgnoreController, mentions, TelemetryService) back to
  mocha; they run on @vscode/test-cli as before.

Verified: check-types 0 errors; compile-tests 0 bun:test in out/;
bun unit 65 files/962 pass/0 fail; vitest 582/0.

* fix(vscode): declare mocha — phantom dep for @vscode/test-cli integration runner

The @vscode/test-cli extension host loads `mocha` at runtime to run the
integration suite, but only @types/mocha was declared (npm hoisted the
mocha package transitively; bun's store does not expose it). The host
failed with "Cannot find module 'mocha'". Declare mocha ^11.7.4 (matches
@vscode/test-cli's own range).

* fix(vscode): robust Windows protoc-gen-ts_proto plugin resolution under bun

build-proto.mjs hardcoded node_modules/.bin/protoc-gen-ts_proto.cmd for
Windows, but bun's workspace store places/extensions the bin shim
differently (hoist + .cmd/.bunx), so Windows protos failed with
"protoc-gen-ts_proto: The system cannot find the file specified". Probe
the local + root .bin with known shim extensions instead. Also update
the testing-platform usage string (ts-node -> bun).

* fix(vscode): generate node .cmd wrapper for ts-proto plugin on Windows

The previous probe found bun's `.bunx` shim, but protoc cannot exec it
("%1 is not a valid Win32 application"). Instead, on Windows generate a
small .cmd wrapper that runs the resolved protoc-gen-ts_proto JS via
`node`, which protoc can execute regardless of package manager. POSIX
path (direct JS bin) is unchanged.

* fix(vscode): package VSIX with --no-dependencies (bundled) to stop monorepo traversal

Under the bun workspace, @cline/* are workspace:* symlinks pointing to
../../../../sdk/packages/*. vsce, walking the dependency tree, followed
them out of apps/vscode and packaged the whole monorepo (../, ~84MB incl.
root node_modules and .env), which crashed vsce's secret scanner and
failed all e2e jobs.

The extension is fully esbuild-bundled into dist/extension.js, so vsce
should not walk node_modules at all. Add --no-dependencies to every
vsce/ovsx package/publish path (e2e build, marketplace, nightly), and
tighten .vscodeignore to drop nested node_modules and dev-only inputs
(scripts, proto, testing-platform, bunfig, esbuild.mjs, etc.).

Result: VSIX is 39 files / ~7 MB and the secret scan passes.

* docs(vscode): tighten bun/node comments and consolidate into a clinerule

- add .clinerules/bun-and-node.md (eternal-now: bun=tooling, node=runtime,
  keep-list, and the bun:test-vs-mocha test routing rule); remove the
  apps/vscode/docs/bun-migration-notes.md migration doc and point
  .clinerules/general.md at the rule (single-line bullet matching the file).
- fix the hotfix-release note: there is no infra step that regenerates the
  lockfile; a CHANGELOG+version bump leaves bun.lock consistent (workspace
  versions aren't pinned) and publish runs --frozen-lockfile.
- reframe runner/preload comments to describe the code as-is (drop
  "migrated off mocha"/codemod history); add a TODO on the bun-test preload
  to migrate suites off the vitest `vi` shim to native bun:test and delete it.
- remove the one-shot mocha->bun codemod scripts.

* fix(debug-harness): pin debugee VSCode version so bundled Playwright can drive it

The harness downloaded "stable" VSCode (currently 1.125 / Electron 42),
which the bundled Playwright cannot drive — `_electron.launch()` hangs
until its 60s timeout (Electron started and a window appeared, but the
launch handshake never completed). Default to a known-good version
(1.103.0, matching the e2e CI matrix) and allow override via
VSCODE_TEST_VERSION.

* fix(webview): render under bun workspace — dedupe React, drop stale codicons link

The webview mounted but crashed before rendering (blank sidebar; e2e
"Login to Cline" never visible) with "Cannot read properties of null
(reading 'useRef')" — the classic two-React-copies / null hook dispatcher.
Under the bun workspace, sibling packages pull react@19 into the shared
store and a transitive webview dep resolved a second React instance into
the vite bundle. Add resolve.dedupe + pin react/react-dom to webview-ui's
own React 18 copy.

Also drop the separate `<link>` to node_modules/@vscode/codicons in the
webview HTML: the webview's index.css already @imports codicons, so the
font is bundled into the build assets. Under bun that node_modules path
is a symlink to the root store (outside the webview localResourceRoots)
and isn't packaged with --no-dependencies, so the link 404'd; the bundle
covers it. Re-scope the .vscodeignore nested-node_modules exclude so it
no longer shadows the codicons re-include.

* fix(debug-harness): disable GPU so the debugee renders in headless/VM envs

On headless/VM GPU stacks the debugee Electron's GPU process crash-loops
("Exiting GPU process during initialization" / CreateCommandBuffer
kTransientFailure), killing the window before Playwright finishes
attaching and tripping the 60s launch timeout. Force software rendering
(--disable-gpu and friends) for a stable harness launch.

* fix(debug-harness): survive launch failures; configurable, longer launch timeout

The harness crashed (whole bun process exited) whenever VSCode launch
failed/timed out: Playwright emits a late unhandled rejection on the dead
CDP transport after we've already handled the launch error, and the
default behavior takes the HTTP server down with it — forcing a full
restart just to retry.

- Add process-level unhandledRejection/uncaughtException guards so stray
  async errors are logged and the server keeps serving (retry via `launch`).
- On launch failure, close the orphaned Electron so a retry isn't blocked.
- Make the _electron.launch timeout configurable (--launch-timeout) and
  raise the default to 120s for cold launches; document VSCODE_TEST_VERSION.

* fix(ci): address review feedback — vsix --no-dependencies, drop stale coverage path, Windows shell

- ext-vscode-publish-stable.yml: add --no-dependencies to the release-artifact
  `vsce package` (Max's catch). Without it, vsce follows the @cline/* workspace
  symlinks out of the package and bloats the .vsix with the whole monorepo.
- ext-vscode-test.yml: drop the stale apps/vscode/coverage-unit/lcov.info upload
  path (Max's catch). That file was produced by the removed nyc unit-coverage
  step (.nycrc.unit.json); nothing generates it now.
- ext-vscode-test-e2e.yml: the better-sqlite3 assert step ran under the Windows
  runner's default pwsh and failed to parse the POSIX test. Pin it to `shell: bash`
  (Git Bash ships on windows-latest); the non-e2e job already defaults to bash.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
2026-06-24 14:11:34 +09:00
Max 8636272eb5 Improve onboarding funnel metrics (#11650)
* improve onboarding metrics

* fix onboarding page view dedupe

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-06-24 14:11:34 +09:00
Max Paulus 🥪 815b0346f6 fix package lock issues post rebase 2026-06-24 14:11:34 +09:00
Max 84fe95de4d fix(vscode): persist Vertex provider settings (#11565)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-06-24 14:11:34 +09:00
Max 36be3333d4 fix(vscode): preserve legacy task metadata on resume (#11570)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-06-24 14:11:34 +09:00
Dominic Cooney 96c0bd70a4 chore(vscode): remove stale HuggingFace provider test 2026-06-24 14:11:33 +09:00
Max Paulus 🥪 6f621e4bf2 fix standalone e2e test 2026-06-24 14:11:33 +09:00
Max Paulus 🥪 d7ea201c25 bump sdk version 2026-06-24 14:11:33 +09:00
Dominic Cooney e3b3e6b0ad fix(vscode): simpler login UX (ENG-2174) — remove inline provider gate, restore debug harness (#11534)
* remove inline no-usable-provider sign-in banner; rely on inference-time errors

The "Sign in to Cline or set up a provider" banner gated chat input on a
parallel provider-usability heuristic that mis-detected BYOK setups
(Bedrock profile/IAM, Vertex ADC) and its sign-in button discarded the
device code. Remove the component and the hasUsableProvider plumbing.

Auth/config problems now surface at inference time, where handling
already exists:
- cline provider without a token -> emitClineAuthError -> ErrorRow
  renders the Sign in button with the device-code display
- any other misconfigured provider -> say:"error" row

Also deletes the now-dead sdk/provider-usability module and adds a test
that failed session start emits a plain chat error.

* restore debug-harness server deleted in 0bfbfb944

Commit 0bfbfb944 ("delete unused files") removed src/dev/debug-harness/server.ts
as dead code, but it is a dev tool launched directly via
`npx tsx src/dev/debug-harness/server.ts` (see its README and
.clinerules/debug-harness.md) — no static import graph reaches it, which
is why the unused-file analysis flagged it. The README, the .clinerules
docs, and the CLINE_CAPTURE_BROWSER / __clineHandleUri hooks in
extension.ts and utils/env.ts that exist solely for this harness all
survived the deletion, leaving them dangling.

Restored verbatim from 0bfbfb944~1; verified it boots and listens on
:19229.
2026-06-24 14:11:33 +09:00
Saoud Rizwan 53d0f50494 fix(vscode): restart session when user switches provider (#11507)
* fix: format Cline OAuth tokens in provider config

* fix(vscode): restart SDK session on provider switch

* fix(vscode): serialize SDK provider restarts

* chore(vscode): keep provider switch PR scoped

* fix(vscode): simplify deferred provider restarts
2026-06-24 14:11:33 +09:00
Mikołaj Kondratek 141a61fd45 fix: thread proxy/CA-aware fetch into the SDK inference path (#11462)
* fix: thread proxy/CA-aware fetch into the SDK inference path

The main agent loop did not receive the host's proxy/CA-aware fetch, so
on JetBrains and the CLI inference over a corporate proxy or to a
self-signed/private-CA endpoint failed with "unable to get local issuer
certificate". This regressed at the SDK cutover: the pre-SDK CLI
(2.18.0) constructed provider clients with a proxy-aware fetch directly,
while the SDK agent loop fell back to bare global fetch (CLINE-2353).

Two layers:
- App (cline-session-factory.ts): always build CoreSessionConfig.
  providerConfig and carry the proxy-aware fetch from @/shared/net, not
  just for Bedrock. In VSCode this fetch is global fetch, so behavior is
  unchanged there; in the standalone (JetBrains) build it is undici with
  EnvHttpProxyAgent.
- SDK (handler-factory.ts): forward providerConfig.fetch into
  createGateway both as the top-level fallback fetch and per provider, so
  the gateway's provider clients use it. Passing undefined is a no-op
  (registry resolves config?.fetch ?? defaults?.fetch ?? fallbackFetch),
  so other SDK consumers are unaffected.

The SDK change covers every host that supplies a fetch; the app change
covers VSCode and JetBrains. The CLI builds its session config through a
separate path (apps/cli) that does not yet wire a proxy-aware fetch, so
CLINE-2353 on the CLI surface is addressed in a follow-up.

Adds a handler-factory unit test asserting the host fetch is forwarded
to createGateway at both the top level and per provider.

* fix: deterministically install proxy dispatcher in standalone core

The proxy/CA-aware undici dispatcher is installed as a side effect of
loading @/shared/net (it calls setGlobalDispatcher with EnvHttpProxyAgent
in the standalone build). The standalone entry cline-core.ts did not
import that module, so the dispatcher was only installed incidentally
when some other transitively-imported module happened to pull it in. A
future change to the import graph could silently drop proxy/CA support on
JetBrains.

Import @/shared/net for its side effect, first, so the install is
deterministic and runs before any network use (CLINE-2353).

Standalone-only hardening; VSCode uses global fetch and is unaffected.
2026-06-24 14:11:33 +09:00
Saoud Rizwan 7578a1de36 fix(vscode): fix duplicate tool row when changing plan/act mode during pending tool approval (#11437)
* fix(vscode): suppress duplicate tool row when a mode change clears a pending approval

Switching plan/act while a tool approval was pending duplicated the
approval row in chat. clearPending resolved the pending approval as
denied, which unblocks the core; the core then emits the denied tool
call's content_start/content_end events before the mode coordinator's
abort lands. The interactive deny paths record the denial in the
message translator state so those events are suppressed, but
clearPending skipped that step, so the translator rendered the events
as a fresh say:tool row next to the still-visible approval ask.

clearPending now records the denial through recordDeniedToolApproval
before resolving, mirroring resolvePendingToolApproval. This covers all
clearPending callers: mode changes, task cancel, and task clear.

* refactor(vscode): trim the clearPending denial fix to its minimal shape

Keep clearPending's original structure, only inserting the denial
recording before the resolve. Drop the end-to-end suppression test:
translator suppression for recorded denials is already covered by
message-translator-approval-denial.test.ts, and the clearPending
recording is covered by the extended unit assertion.
2026-06-24 14:11:33 +09:00
Saoud Rizwan 5aa60cf116 fix(vscode): restore aggressive pin-to-bottom auto scroll in chat view (#11436)
* fix(webview): restore aggressive pin-to-bottom auto scroll in chat view

The auto-scroll effect only fired on groupedMessages.length changes, but in
the SDK-migrated extension new content can appear in the chat without the
message list length changing:

- The Thinking placeholder row is driven by turnState alone (e.g. the plan
  to act switch auto-continues the task with no new message), and it was
  appended to the rendered list inside MessagesArea where the scroll hook
  never saw it.
- New tool messages merge into the trailing tool group, and the thinking
  placeholder gets swapped for a real reasoning row at constant length.

Fixes:
- Lift the thinking placeholder computation out of MessagesArea into a new
  useDisplayedGroupedMessages hook so ChatView feeds the same list to both
  Virtuoso and useScrollBehavior; the placeholder appearing now pins to
  bottom like a real message.
- Key the pin effect on the tail message ts (skipping the placeholder) in
  addition to list length, covering in-place tail changes.
- Re-engage auto scroll when turnState.phase transitions into streaming. In
  the old extension every turn start was accompanied by a user send/button
  click that reset disableAutoScrollRef; turnState-driven turn starts like
  plan to act auto-continue have no webview-side action, so handle it in
  the scroll hook.

* refactor(webview): replace scroll fix with minimal single-file version

Same three behaviors as the previous commit (pin when the thinking
placeholder appears, pin on in-place tail changes, re-engage auto scroll
when a turn starts streaming) but implemented as two small effects in
MessagesArea, which already has both the rendered list and scrollBehavior
in scope. Reverts the useDisplayedGroupedMessages hook extraction and the
ChatView/useScrollBehavior changes; net diff vs the base branch is now
one file.
2026-06-24 14:11:33 +09:00
Dominic Cooney b62e572ea9 test(vscode): exercise full SDK structured edit flow in file-edit e2e (#11442)
* test(vscode): exercise full SDK structured edit flow in diff.test.ts e2e (ENG-2042)

The SDK runtime executes structured (OpenAI-format) tool calls instead of parsing XML-style tool syntax out of assistant text. Teach the e2e mock server to stream an editor tool call for edit_request (arguments split across deltas to exercise fragment reassembly), answer the SDK's follow-up tool-result request (role:'tool' message) with turn-ending completion text, and remove the classic XML-era EDIT_REQUEST/REPLACE_REQUEST responses.

diff.test.ts now covers the full approval flow: approval ask row -> Save -> editor tool writes the file -> completion text, verifying the edit on disk and restoring the git-tracked fixture afterwards. The old 'test.ts: Original <-> Cline's Changes' diff-tab assertions are unreachable under the SDK executor architecture (the editor executor writes via Node fs and does not route through DiffViewProvider); this behavioral difference is documented in the test file.

* test(vscode): address review feedback on diff.test.ts e2e

- Scope the mock server's tool-result follow-up detection to edit_request conversations so tool results from other (future) scenarios don't mis-route to EDIT_REQUEST_COMPLETE.

- Move the fixture readFileSync inside the try block and guard the finally restore, so a failed read doesn't bypass cleanup attribution or write undefined back to the fixture.

* docs(vscode): rephrase diff e2e comments to describe current behavior

Comments described historical behavior (XML-style tool-call parsing that predates the SDK runtime), which is confusing to readers of the current code. Rephrase them to describe the code as it exists now.

* test(vscode): rename diff.test.ts to file-edit.test.ts and drop duplicated preamble

The test no longer touches a diff editor (the SDK editor executor writes files directly after approval), so the 'Diff Editor' name was misleading. Rename the file and describe block to match what it asserts: the file-edit approval flow.

Drop the first half of the test (send hello, wait, New Task, check history), which duplicated chat.test.ts, and the mock server's 500ms delay that existed only to support an 'API Request...' visibility assertion that no longer exists.
2026-06-24 14:11:33 +09:00
Robin Newhouse 268462ddcf fix(vscode): stabilize SDK e2e login flow (#11441) 2026-06-24 14:11:33 +09:00
Dominic Cooney 072e237d01 fix(vscode): persist skill disable to SKILL.md frontmatter so the model honors it (ENG-1995) (#11294)
* fix(vscode): persist skill disable to SKILL.md frontmatter so the model honors it (ENG-1995)

The VS Code skill toggle only updated extension state (globalSkillsToggles /
localSkillsToggles), but the SDK builds the model's skill list and the `skills`
tool from each SKILL.md's frontmatter `disabled` flag. As a result, disabling a
skill in the sidebar left it fully available to the model, including in new
tasks.

toggleSkill now also writes the `disabled` flag to the skill's SKILL.md
frontmatter (no-op for remote skills, which have no backing file), via new
helpers updateSkillMarkdownDisabledState / setSkillDisabledInFrontmatter in
skills.ts. Adds unit tests for both helpers.

* fix(vscode): don't rewrite skills with malformed frontmatter (ENG-1995)

parseYamlFrontmatter fails open on invalid YAML, returning the full original
document as the body. updateSkillMarkdownDisabledState would then prepend a
second `---` block on a disable, corrupting the file. Bail out and leave the
file untouched when frontmatter fails to parse. Adds tests for the malformed
disable/enable cases.

Addresses Greptile review feedback on #11294.

* test(vscode): assert malformed-skill fixture is actually invalid YAML (ENG-1995)

Add a guard test that parseYamlFrontmatter reports hadFrontmatter and a
parseError for the shared malformed fixture, so the two "leave file untouched"
tests can't silently pass via a different code path if the fixture ever became
valid YAML.

Addresses Greptile review feedback on #11294.

* fix(vscode): resolve @cline/shared/storage subpath in mocha unit-test compile

The CommonJS mocha unit-test runner uses classic "node" moduleResolution,
which does not read the `exports` subpath maps in @cline/* package
manifests, so `@cline/shared/storage` (imported by
src/sdk/telemetry-settings-sync.ts) failed with TS2307 when test files
transitively reach the SDK adapter. Mirror the explicit paths mapping
already added to tsconfig.test.json for the integration-test compile.

* fix(vscode): restore E2E mock auth in SDK auth service so e2e tests can sign in

The SDK migration replaced classic AuthService (which swapped in
AuthServiceMock under E2E_TEST) with sdk/auth-service.ts, losing the
mock path. "Login to Cline" then invoked the real SDK OAuth flow and
opened a native browser dialog the Playwright tests cannot interact
with, so helper.signin() never authenticated and chat.test.ts +
diff.test.ts failed on every platform (the failures also reproduce on
the base branch).

- auth-service.ts: under E2E_TEST=true (and CLINE_ENVIRONMENT=local),
  exchange the well-known test code with the local mock API server and
  persist credentials to providers.json — no browser. Replaces classic
  AuthServiceMock (see origin/main src/services/auth/AuthServiceMock.ts).
- chat.test.ts/diff.test.ts: wait for the mock turn to complete before
  clicking New Task; SDK history is persisted at turn end, so navigating
  mid-turn races the write and Recent never shows.
- diff.test.ts: the footer Start New Task button only appears for
  attempt_completion turns under SDK TurnState; use the header New Task
  button like chat.test.ts.
2026-06-24 14:11:32 +09:00
Saoud Rizwan 31cff1f75e fix(vscode): auto-continue the task when switching from plan to act (#11401)
* fix(vscode): enforce stop-before-start ordering for same-id session restarts

The app reuses the taskId as the sessionId whenever it replaces or
resumes a session (mode/MCP rebuilds, follow-up resume, history
restore), but the old session's stop ran fire-and-forget, and core
cleanup is keyed by sessionId across multiple awaits. A stop still in
flight when the same-id replacement started could tear down the live
successor: late sessions-map deletes, a late 'ended' emission, or a
stalled status write landing on the replacement.

Adopt the sequencing invariant the CLI has always used: never start a
same-id session while its stop is in flight. SdkSessionLifecycle tracks
in-flight stops in a pendingStops map keyed by sessionId, and
startNewSession awaits the pending stop for a reused id before starting
(with a log line so a wedged stop is diagnosable). Fresh-id starts
never wait. fireAndForgetSend additionally captures the ActiveSession
by object identity at send time so a send settling after a same-id
replacement cannot flip the successor's run state.

* fix(vscode): auto-continue the task when switching from plan to act

In plan mode, the model's switch_to_act_mode tool call flipped the toggle
but ended the run as aborted: the beforeModel stop hook fired after
turn-started, leaving a dangling api_req_started spinner rendered as
'API Request Cancelled', and nothing continued the task after the
act-mode rebuild. Manually toggling after a presented plan had the same
dead end.

The tool now declares lifecycle.completesRun so the run ends cleanly
after the tool result, and the queued mode change rebuilds the session
and auto-continues with a hidden continuation prompt. A manual plan to
act toggle auto-continues only when the agent is idle after presenting
its plan (not running and awaiting_followup; a pending ask_question
blocks mid-run so it cannot false-positive). Composer content rides
along: typed text becomes the continuation, attachments are forwarded
and echoed, attachment-only toggles count as consumed. The RPC reports
consumption only after the send was actually handed to the session, and
the webview then clears only the exact submitted content, so failures
and racing input never lose composer state. Failures before the send
undo the optimistic running flip, report an error phase, and roll the
mode back when the session was never replaced.

Hidden prompts (the act continuation and the pre-existing task
resumption prompt) shifted editMessageAndRegenerate's visible-to-SDK
user message ordinal mapping; the new sdk-user-message-mapping module
skips them in their persisted user_input-wrapped shape, counts
attachment-only messages (which have visible bubbles), ignores
tool-result rows, and attachment-only resumes now echo a bubble to keep
both transcripts aligned. Follow-ups sent during a rebuild wait on
waitForPendingRebuild instead of resuming a parallel session that the
rebuild would kill.

The plan-mode system prompt and tool description require explicit user
approval in a message sent after the plan was presented, preventing the
model from self-escalating to act mode.

* fix(vscode): move the turn phase to error when a task resume fails

askResponse optimistically sets the turn phase to streaming before
delegating to the followup coordinator, but the coordinator's resume
catch only posted an error row, leaving the footer stuck on
Thinking/Cancel. Resume failures (auth errors, session start errors)
now report back via onResumeFailed so the controller can set the phase
to error.
2026-06-24 14:11:32 +09:00
Saoud Rizwan 9a981f81d7 fix(webview): use consistent reasoning selector component in extension provider settings (#11399)
* fix(webview): use themed components and reasoning selector in generic provider settings

The catalog-backed GenericProviderSettings path (deepseek, gemini, mistral,
and other migrated providers) rendered its model picker with raw unstyled
HTML select/input/button elements, unlike every other provider which uses
the VS Code webview-ui-toolkit components. Swap ModelPickerWithManualEntry
to VSCodeDropdown/VSCodeOption/VSCodeTextField/VSCodeButton, reusing the
DropdownContainer and re-init key workaround from common/ModelSelector.

Also render ReasoningEffortSelector in GenericProviderSettings when the
selected model's catalog info has supportsReasoning, persisting the effort
through the provider config reasoning patch, matching ClineModelPicker.
This is driven by the catalog capability flag rather than provider id.

* fix(webview): re-sync custom model id field after async config hydration

The controlled customModelId state was initialized once at mount, but the
provider config and model catalog both hydrate asynchronously, so the lazy
initializer could capture a placeholder value and leave the custom model
text field stale once the committed selection loaded. Sync the field via an
effect keyed on the committed model id and its in-list status, depending on
derived values rather than the models object whose identity can change
every render while the catalog loads.
2026-06-24 14:11:32 +09:00
Robin Newhouse d8b1ce54d4 fix(vscode): expand remote workflow/skill slash commands before send ENG-2036 (#11388)
* fix(vscode): expand remote workflow/skill slash commands before send

The SDK-backed extension sent `/workflow` text to the model verbatim, so
remote-config workflows never ran. Expansion is host-driven (the agent loop
never auto-expands), and the controller's pre-send path did none — matching
the CLI's `buildUserInputMessage`, resolve slash commands via a
controller-owned UserInstructionConfigService that watches the workspace
(including `.cline/remote-config/`), refreshed after each remote-config sync.

Fixes ENG-2036.

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

* fix(vscode): guard instruction watcher against post-dispose race

Reject in ensureUserInstructionService when the controller is already
disposed so a slash-command resolution that yielded across dispose() can't
resurrect a file watcher that nothing will stop. Also log the post-expansion
length handed to parseMentions. Addresses Greptile review.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 14:11:32 +09:00
Max Paulus 🥪 6bc771ba9d include optional deps so that CI passes 2026-06-24 14:11:32 +09:00
Max Paulus 🥪 87338cacd1 fix broken tests 2026-06-24 14:11:32 +09:00
Max Paulus 🥪 827a4a2616 bump sdk version 2026-06-24 14:11:32 +09:00
Max Paulus 🥪 13d6fe3876 add vertex support to extension 2026-06-24 14:11:32 +09:00
Mikołaj Kondratek 82c3d8d771 fix(sdk): make model-not-found API errors actionable in the webview (#11378)
When a provider returns a model-not-found error (e.g. Anthropic's HTTP 404
for a retired model such as claude-3-haiku-20240307), the SDK strips the
status and delivers only the terse body, which collapses to the bare label
"model: <id>". reshapeErrorForWebview fell through to returning that raw
string, so ErrorRow rendered a label-like fragment in red with no hint that
the model is gone or how to recover.

Detect these in the plain-text branch of reshapeErrorForWebview and rewrite
them into a sentence that names the model and tells the user to switch models
in API Configuration settings, then retry. The model switch is framed as a
precondition rather than a parallel option so users don't loop on Retry.

Detection is text-based because the HTTP status is unavailable at this point.
The keyword match is anchored to the word "model" with a not-found signal in
the same sentence, so unrelated errors that merely mention a model (plan
gating, deprecated features) are left untouched. Adds tests for the bare
label form, a generic "does not exist" form, and two negative cases (plan
gating and an auth error mentioning a model) that must pass through unchanged.
2026-06-24 14:11:32 +09:00
Max Paulus 🥪 ee0c63a216 fix telemtry opt flag migration 2026-06-24 14:11:31 +09:00
Dominic Cooney 11e668ca23 fix(vscode): resolve @cline/shared/storage subpath in test compile + vitest
The CommonJS integration-test tsconfig (moduleResolution: node) and the
vitest config did not resolve the @cline/shared/storage exports subpath
imported by src/sdk/SdkController.ts, breaking 'compile-tests' (TS2307)
and 3 vitest SDK suites. Add explicit path/alias mappings to the built
dist so both resolve without changing module emit. Compile-time/test-only;
emitted JS still uses the real package specifier.
2026-06-24 14:11:31 +09:00
Max Paulus 🥪 50a4bfb435 migrate telemetry value in extension 2026-06-24 14:11:31 +09:00
Max Paulus 🥪 2c53d36e6d bump sdk version 2026-06-24 14:11:31 +09:00
Max Paulus 🥪 e4ab360c3c fix claude-code setting loading/persistence 2026-06-24 14:11:31 +09:00
Max Paulus 🥪 5363b2a6d2 fix task history delete 2026-06-24 14:11:31 +09:00
Max Paulus 🥪 51d3865ec5 fix model selector not showing most up to date model in providers.json 2026-06-24 14:11:31 +09:00
Max Paulus 🥪 2a070cddf5 fix ui test 2026-06-24 14:11:31 +09:00
Max Paulus 🥪 401154a42e fix ci checks 2026-06-24 14:11:31 +09:00
Robin Newhouse 3b02d5162e refactor(vscode): remove MCP marketplace ENG-1591 (#11217)
* refactor(vscode): remove MCP marketplace

* test(vscode): clarify MCP marketplace removal test

* docs: update MCP server controls docs
2026-06-24 14:11:31 +09:00
Max Paulus 🥪 710cceab90 fix anthropic provider settings persistence 2026-06-24 14:11:30 +09:00
Max Paulus 🥪 a469377d57 remove baseUrl from providers.json when unchecking box in ui 2026-06-24 14:11:30 +09:00
Max Paulus 🥪 1689abf479 fix ollama and lmtudio settings persistence 2026-06-24 14:11:30 +09:00
Max Paulus 🥪 abd84b80cb fix openrouter apikey persist to providers.json 2026-06-24 14:11:30 +09:00
Max Paulus 🥪 9d4aa03d4d fix vscodelm provider settings persist 2026-06-24 14:11:30 +09:00
Max Paulus 🥪 91077fd49e persist bedrock settings to providers.json 2026-06-24 14:11:30 +09:00
Max Paulus 🥪 6aa4c33918 don't block user input when hasNoUsableProvider == true 2026-06-24 14:11:30 +09:00
Mikołaj Kondratek aa8e11ad47 fix(bedrock): treat profile/IAM/credential-chain auth as a usable provider (#11313)
The inline "Sign in to Cline or add an API key" gate appeared and disabled
chat for Amazon Bedrock users who configured AWS Credentials (access key +
secret), an AWS profile, or relied on the default AWS credential chain, even
though the provider was fully usable (issue #11270).

hasUsableProvider() decided Bedrock usability solely via resolveApiKey(),
which maps bedrock -> awsBedrockApiKey. Bedrock's three non-API-key auth
modes leave that field empty, so buildBedrockProviderConfig() would build a
working session while the gate reported the provider unusable. The Cline
login state is irrelevant here: the gate is computed for the active-mode
provider, and the "Sign in to Cline" button is just one of two generic
remedies, which is what made the symptom look like a logged-out state.

Add a Bedrock branch that classifies usability per auth mode, reusing
resolveBedrockAuthentication() so the gate and the session builder agree on
what each mode means:
- api-key: usable only when awsBedrockApiKey is non-blank (unchanged, now
  also rejects whitespace-only keys)
- profile / iam / default credential chain: usable, deferring credential
  resolution to request time (mirrors buildBedrockProviderConfig and the
  existing keyless-provider philosophy)

Manually verified on a real setup across all four auth modes: pre-fix the
gate blocked chat for access-key and profile auth; post-fix the gate clears
and chat works. API-key mode was never gated incorrectly.

Tests: add Bedrock coverage for every auth mode, including api-key with a
blank and with an unset key (both not usable), the SigV4 repro, profile
(explicit/inferred/awsUseProfile), the bare credential-chain config, and
plan-mode resolution plus plan/act isolation.
2026-06-24 14:11:30 +09:00
Ara ca128cfd9f Fix SDK task size in delete tooltip (#11277)
* fix: show SDK task size in delete tooltip

* fix: address SDK task size review feedback

* fix: simplify SDK task size caching
2026-06-24 14:11:30 +09:00
Max Paulus 🥪 78b1f30133 remove unused code/files
fix broken webview build

remove unused code/files

fix broken webview build
2026-06-24 14:11:30 +09:00
Max Paulus 🥪 1f31738b32 delete unused files 2026-06-24 14:11:29 +09:00
Max Paulus 🥪 6eeee7c4e6 Add edit and regenerate for VS Code chat messages
Allow user feedback messages in the VS Code extension to be edited inline and regenerated from that point. Adds a TaskService RPC, truncates persisted SDK history before the selected visible user prompt, and starts a new session with the edited prompt. Also ensures the regenerated active task appears in extension history while SDK history catches up.
2026-06-24 14:11:29 +09:00
Max Paulus 🥪 e88e0c4994 fix webview-ui tests 2026-06-24 14:11:29 +09:00
Max Paulus 🥪 4258b338df show model list if possible for openai compatible 2026-06-24 14:11:29 +09:00
Max Paulus 🥪 7b24026291 fix onboarding model selection not persisting 2026-06-24 14:11:29 +09:00
Max Paulus 🥪 9a6c972b02 remove provider-specific views and just use genericprovidersettings.tsx 2026-06-24 14:11:29 +09:00
Max Paulus 🥪 fae824bff0 dry up duplicate code and create useProviderModelSelection 2026-06-24 14:11:29 +09:00
Max Paulus 🥪 9011051f76 dry up provider api key logic 2026-06-24 14:11:29 +09:00
Max Paulus 🥪 c4068823a4 dry up some duplicate code 2026-06-24 14:11:29 +09:00
Max Paulus 🥪 323492108d fix onboarding models 2026-06-24 14:11:28 +09:00
Max Paulus 🥪 58b41fbaf5 fix failing biome/lint 2026-06-24 14:11:28 +09:00
Mikołaj Kondratek 3df2d6c780 Remove unused import 2026-06-24 14:11:28 +09:00
Mikołaj Kondratek 60b1ab4d57 fix(sdk): drop dead autoContinue branch in mode rebuild
cf25cd66a ("make extension plan mode more similar to CLI") removed the
file-level ACT_MODE_CONTINUATION_PROMPT constant and stopped passing the
autoContinue / continuationPrompt options when rebuilding a session for
a mode change, but left the corresponding block inside
rebuildSessionForMode in place. The block still references the deleted
constant, so tsc fails on the SDK migration branch with TS2304: Cannot
find name "ACT_MODE_CONTINUATION_PROMPT".

No caller passes options to rebuildSessionForMode anymore, so the block
is dead. Drop the block and narrow the signature to take only newMode.
Existing tests already invoke rebuildSessionForMode(<mode>) with no
second argument and assert that fireAndForgetSend is not called on a
mode rebuild, so they keep passing.
2026-06-24 14:11:28 +09:00
Mikołaj Kondratek 0755b58da1 fix(terminal): capture standalone terminal output on Windows and harden PowerShell command handling (#11133)
* fix(terminal): surface standalone terminal spawn diagnostics

Add Logger calls at every chokepoint of the standalone terminal pipeline
so the (currently silent) failure modes around JetBrains-hosted
cline-core become debuggable from cline-core-service.log.

Lines added, all using the existing Logger facility (no new
dependencies, no behavioral changes):

* StandaloneTerminalProcess.run() now logs:
  - `[StandaloneTerminalProcess] run() entered: shell=… cwd=… args=…`
    on entry, before the try block;
  - `[StandaloneTerminalProcess] spawned pid=… for shell=…` right
    after child_process.spawn returns;
  - `[StandaloneTerminalProcess] close: code=… signal=… fullOutputLen=…`
    inside the `close` handler (the `fullOutputLen` reveals when the
    child exits 0 with empty pipes — the symptom in issue #10948);
  - `[StandaloneTerminalProcess] child error: …` in the `error`
    handler;
  - `[StandaloneTerminalProcess] spawn threw synchronously: …` in
    the outer catch.

* StandaloneTerminalManager.runCommand() now logs entry
  (`[StandaloneTerminalManager] runCommand terminalId=…: <cmd>`) and
  attaches a `.catch` to the previously fire-and-forget
  `process.run(…)` Promise so an unhandled rejection surfaces as
  `[StandaloneTerminalManager] process.run rejected for terminal …`
  instead of disappearing.

* CommandExecutor.execute() extends the existing "Executing command
  in … terminal" line with `mode=<terminalExecutionMode>` and
  `managerCtor=<manager.constructor.name>`, so it's possible to
  confirm whether the `vscodeTerminal` path is in fact backed by a
  `StandaloneTerminalManager` on JetBrains (it is — see
  notes/issue-10948-…md).

* CommandOrchestrator.orchestrateCommandExecution() logs the
  `process.once("completed")` event with `exitCode`/`signal`/
  `terminalType`, the "resolved completed" return branch with the
  line/byte totals, and emits a `WARN` on the silent "still running"
  fall-through. The last one matters because the original repro
  reported "Command executed successfully (exit code 0)" with empty
  output — the WARN makes that branch loud the next time it fires.

These logs are what made the two distinct bugs in #10948 visible
(see the 2026-05-28 update in
notes/issue-10948-terminal-output-investigation-2026-05-27.md). They
stay in to keep the next regression debuggable.

Refs: cline/cline#10948

* fix(terminal): keep Windows child stdio attached to parent pipes

The non-cmd Windows branch in StandaloneTerminalProcess.run() spawned
the shell (powershell.exe in practice) with `detached: true` and no
`windowsHide`. When cline-core is launched by the JetBrains plugin it
has no console of its own, so Windows CreateProcess allocates a NEW
console for the detached child and the child's stdio routes to that
new console instead of the pipe handles the parent created. From the
parent's point of view the pipes immediately EOF, `close` fires with
`code=0`, and `fullOutput` is 0 bytes — exactly the symptom reported
in cline/cline#10948 ("Command executed successfully (exit code 0)"
with no output and no filesystem effect).

This bug applies to every command the agent runs through the
standalone terminal path on Windows, not just the
double-wrapped-PowerShell cases (verified by re-running a clean
`dir <file>` after the diagnostics from the previous commit landed:
`run() entered` and `spawned pid=<num>` both fired, then `close: code=0
fullOutputLen=0`).

Fix:

* `detached: process.platform !== "win32"` — keep the existing
  POSIX behavior (a separate process group helps `tree-kill`), but
  drop it on Windows where `tree-kill` walks the PID tree with
  `taskkill /T` and doesn't need a process group.
* `windowsHide: true` — matches every other `child_process.spawn`
  call site in cline-core (git, MCP, hooks, browser) and flips on
  `CREATE_NO_WINDOW`, keeping the child attached to our pipes
  without popping a console window.

Verified on Windows 11 + IntelliJ IDEA 2026.1 + Cline plugin
1.1.59-Internal: `dir <path>`-style probes now produce a non-zero
`fullOutputLen` in the close log, and the captured output bytes
match what would have been visible interactively. PowerShell
double-wrapping (the other half of #10948) is handled in a
follow-up commit.

Refs: cline/cline#10948

* fix(terminal): harden PowerShell command wrapping for standalone shell

`StandaloneTerminalProcess.getShellArgs()` blindly wrapped every
PowerShell command as `["-Command", command]`. That has two
end-user-visible failure modes on Windows, both observed in
cline/cline#10948:

1. The agent's `run_commands` tool call sometimes arrives already
   prefixed with `powershell -Command "…"`. We then spawned
   `powershell.exe -Command 'powershell -Command "…"'`, and the
   outer shell shredded the inner single/double-quote pairs while
   re-parsing its `-Command` argument. The inner pwsh saw
   quote-empty `Test-Path` calls, fell through to the `else` branch
   and reported "File not found" — to ITS stdout, which the outer
   inherited but the file deletion the LLM intended never ran.
2. The user's `$PROFILE` script ran on every spawn, leaking
   non-deterministic noise (e.g.
   `%windir%\System32\REG.exe : The module '%windir%' could not be
   loaded`) into the captured output and confusing the agent.
3. Bonus: the POSIX branch used `["-l", "-c", command]`. The `-l`
   re-sources login files on every command, which is slow and lets
   greeter scripts leak into output.
4. Bonus: the cmd branch used `["/c", command]`. `/d` skips
   AutoRun, `/s` makes the embedded-quote handling deterministic.

Fix:

* PowerShell branch returns
  `["-NoProfile", "-NonInteractive", "-Command", unwrap(command)]`.
  `-NoProfile` suppresses (1) the spurious profile noise that
  contaminated the captured output, and `-NonInteractive` ensures
  the child doesn't deadlock waiting on a prompt no one will answer.
* `unwrapPowerShell(command)` strips a leading
  `powershell|pwsh [.exe] -Command|-c "…"` (or single-quoted)
  wrapper that the LLM sometimes emits, fixing the double-pass
  argument-quoting destruction. If the command does not match the
  exact wrapper shape it is returned verbatim — worst case is "no
  change", preserving pre-fix behavior.
* cmd branch returns `["/d", "/s", "/c", command]`, matching the
  canonical helper in cline/sdk/packages/shared/src/parse/shell.ts.
* POSIX branch returns `["-c", command]`, dropping the unhelpful
  `-l`. Also matches the SDK helper.

Verified on Windows 11 + IntelliJ IDEA 2026.1 + Cline plugin
1.1.59-Internal in combination with the previous "keep Windows
child stdio attached" commit: `Remove-Item CHANGELOG.md` now
deletes the file, the agent's verification `Get-ChildItem CHANGELOG*`
returns nothing, and the profile-load REG.exe error no longer leaks
into captured output.

Refs: cline/cline#10948

* refactor(terminal): tone down standalone terminal diagnostics

The diagnostics added while chasing #10948 were intentionally loud so the
two bugs were visible. Now that the fixes are in, reduce them to a normal
operating posture:

* Demote fine-grained traces to `debug`: the per-spawn `spawning …` and
  `spawned pid=…` lines, `StandaloneTerminalManager.runCommand`, and the
  orchestrator's `resolved completed` summary.
* Drop the orchestrator's `completed event` line entirely — the
  `resolved completed` debug line already carries exit code, signal, and
  line/byte totals.
* Stop echoing the full command in the manager line and stop echoing the
  args vector in the spawn line. The command is still logged once at
  `info` by CommandExecutor (unchanged, pre-existing), so we go back from
  three command echoes to one. Commands routinely embed secrets
  (Authorization headers, tokens), so fewer copies on disk is better.

Kept loud on purpose:

* `info` on `close: code=… fullOutputLen=…` — the single line that proves
  the Windows stdio-capture fix and the most useful per-command signal.
* `warn` on `resolved without completion event` — the silent-success
  canary for the #10948 failure mode.
* `error` on child error / synchronous spawn failure / unhandled
  process.run rejection.

Refs: cline/cline#10948

* fix(terminal): tighten PowerShell unwrap regex and extract to a pure module

Two review follow-ups for the #10948 shell-arg handling:

1. The wrapper-strip regex used a greedy `([\s\S]*)` body, so a command
   like `powershell -Command "foo" "bar"` would match with the body
   captured as `foo" "bar`, silently rewriting a command into something
   different. Replace the body with a tempered match `((?:(?!\1).)*)`
   that cannot contain the captured delimiter, so anything other than
   exactly one quoted token is returned verbatim. Worst case is now
   "no change" rather than an incorrect rewrite. The legitimate
   double-wrapped case from #10948 (outer ", inner ') still unwraps.

2. `getShellArgs` and `unwrapPowerShell` were private methods on
   StandaloneTerminalProcess, untestable without spawning a process.
   Move them to a pure `shellArgs.ts` module. `getShellArgs` now takes
   an injectable `platform` (defaulting to `process.platform`) purely so
   the win32-vs-posix branch is testable; behavior is unchanged. This
   also gives us a single local seam to later consolidate onto the
   canonical `@cline/shared` helper (tracked as a follow-up).

No behavioral change beyond the regex correctness fix.

Refs: cline/cline#10948

* test(terminal): cover shell-arg construction and PowerShell unwrap

Add mocha unit tests (matching the repo's node:assert/strict + __tests__/
convention so the existing mocharc spec globs pick them up) for the newly
extracted shellArgs module:

* unwrapPowerShell: double-quote and single-quote wrappers, powershell.exe
  -c form, the #10948 nested-quote repro (inner quotes preserved),
  non-wrapped passthrough, and the two regressions the tightened regex
  must reject (`… "foo" "bar"` and a command that merely mentions
  powershell mid-string).
* getShellArgs: PowerShell -> -NoProfile -NonInteractive -Command (with
  unwrap), cmd -> /d /s /c, POSIX -> -c. The injectable platform arg lets
  these run on any CI host.

This closes the M1 review finding (the regex was the riskiest line in the
change and had zero coverage) and exercises the cmd/POSIX flag changes
called out in M2.

Refs: cline/cline#10948

* docs(terminal): drop issue references and clarify windowsHide comment

Remove inline issue-number references from source comments and a test
name; that context belongs in the commit history, not the code. Also add
a one-line note that windowsHide is a no-op on non-Windows platforms,
since it is set unconditionally while the surrounding comment is
Windows-specific.

No behavior change.

* refactor(terminal): drop warn on the non-completion return path

The orchestrator's final fall-through return is a normal, expected path:
the process resolved via `continue` without a `completed` event (e.g. a
terminal mode without shell integration, or proceed-while-running flows).
Logging it at `warn` cries wolf on healthy runs, so remove it. The
genuine failure mode this was meant to catch surfaces through the
`close`/error logs and the result string itself.

* fix(terminal): address review feedback on standalone spawn paths

Three follow-ups from code review:

* StandaloneTerminalManager.runCommand: the unawaited process.run()
  .catch only logged. run() emits "error" for failures it catches, but a
  rejection escaping without an "error" event would leave the outer
  promise (resolved via the "continue"/"error" events) pending forever,
  stalling the caller. Re-emit "error" from the catch so both paths stay
  consistent. Cannot trigger today (no await outside run()'s try/catch)
  but the guard exists precisely for future rejections.

* shellArgs POSIX branch: document that dropping the login flag (`-l`)
  is intentional and relies on the child inheriting the parent's PATH via
  process.env, with a note that a GUI-launched IDE without a login PATH
  is the edge case to watch.

* StandaloneTerminalProcess cmd.exe branch: add windowsHide:true. The
  console-allocation/window-pop problem is not exclusive to the non-cmd
  branch; a console-less parent could pop a window for cmd.exe too.
  No-op on non-Windows.
2026-06-24 14:11:28 +09:00
Ara 9c030a93b6 Remove Explain Changes feature (#11278)
* chore(vscode): remove explain changes entry points

* chore(vscode): remove explain changes feature
2026-06-24 14:11:28 +09:00
Max Paulus 🥪 f9fcf7f5ce make extension plan mode more similar to CLI
- basically, don't auto continue when agent switches to act mode
2026-06-24 14:11:28 +09:00
Max Paulus 🥪 c4cf2743d6 fix zai insufficient credits issue 2026-06-24 14:11:28 +09:00
Max Paulus 🥪 1dbfb50038 fix tool use name sanitization 2026-06-24 14:11:28 +09:00
Max Paulus 🥪 9b548347ef fix broken tsc 2026-06-24 14:11:28 +09:00
Dominic Cooney 2dc5791d46 fix(vscode): exclude vitest src/sdk suites from CommonJS test compile
compile-tests runs 'tsc -p tsconfig.test.json' (module: commonjs) over all
src/**/*.test.ts for the VS Code integration runner. The new src/sdk vitest
suites use top-level 'await import(...)' (after vi.mock), which is invalid
under CommonJS and fails with TS1378. The integration runner never runs
src/sdk anyway (.vscode-test.mjs only globs core/test/utils/shared/
integrations/hosts/services); these run via 'npm run test:vitest'. Exclude
src/sdk/**/*.test.ts from the integration compile.
2026-06-24 14:11:27 +09:00
Dominic Cooney 35e3bb4787 fix(vscode): restore biome --config-path so lint resolves apps/vscode/biome.jsonc
The rebase dropped '--config-path ./biome.jsonc' from the lint/format/
postprotos scripts and removed the '!!**/.vscode-test' ignore from
biome.jsonc. Without the explicit config path, biome auto-discovered the
root biome.json instead of apps/vscode/biome.jsonc, applying the wrong
rule severities (449 errors at error level for rules that are off/info in
the nested config). Restore both to match origin/main and apply the
pending buf format fix to models.proto.
2026-06-24 14:11:27 +09:00
Dominic Cooney 205a539491 fix(vscode): show running state for in-progress commands
The command row reflects an executing state while a command runs. The
message translator includes the command-output marker on the running
command row so the webview renders it as executing; the row is finalized
with output and a completed flag when the command ends.

Also remove the unused onChange parameter from the foreground run_commands
path: the SDK runtime does not pass it, so it had no effect. Foreground
command output is surfaced to the chat at completion, not incrementally.

Fixes CLINE-2298 and CLINE-2162
2026-06-24 14:11:27 +09:00
Dominic Cooney 0175548dd8 fix(vscode): re-enable approval buttons for consecutive asks
The footer Approve/Reject buttons stayed disabled when a second consecutive
approval ask arrived. The button configs are shared singletons (e.g.
BUTTON_CONFIGS.tool_approve), so two identical asks return the same object
reference and the effect that reset the processing latch never re-ran.

Key the processing latch on the ask identity (anchored turn timestamp plus the
button labels) rather than the config object reference, using a ref-based latch
so each new ask re-enables the buttons. Adds a regression test.

Test plan:

1. Ask the agent to generate two requests to ls /tmp at once

2. Approve (or reject) the first request

3. Check that the buttons for the second request are enabled
2026-06-24 14:11:27 +09:00
Dominic Cooney 6acc231da5 feat(vscode): add the VS Code Language Model (vscode-lm) provider
Run Cline inference through the VS Code Language Model API (vscode.lm), enabling
models contributed by any extension that registers a language model chat
provider with VS Code. GitHub Copilot is the most common such vendor, but the
implementation is vendor-agnostic — it selects models via
vscode.lm.selectChatModels and has no Copilot-specific logic.

- VsCodeLmHandler implements the Cline SDK ApiHandler and is registered with the
  SDK handler registry; the model selector travels as a vendor/family[/version/id]
  string in modelId and is parsed back here. Selector segments are
  percent-encoded so values containing slashes round-trip intact.
- Native tool calling: tool definitions are passed to sendRequest and tool calls
  are surfaced as tool-call chunks; tool results round-trip as
  LanguageModelToolResultPart, with structured tool output serialized to text and
  a trailing user message appended when a turn ends on tool results so models can
  read the output.
- Gated to VS Code: registration is conditioned on the vscode.lm API being
  present, and the provider is hidden in the UI on hosts without it (JetBrains).

Depends on @cline/{shared,llms,agents,core} 0.0.42-nightly.1780514867, the first
published SDK build with the custom-registered-handler routing this provider
needs.
2026-06-24 14:11:27 +09:00
Ara d2ac39455b Fix approval chat replies rendering as tool errors (#11246)
* fix(vscode): route approval chat replies as user feedback

* fix(vscode): suppress approval reply denial errors

* fix(vscode): hide rejected approval tool failures

* chore(vscode): clarify denied approval suppression helper
2026-06-24 14:11:27 +09:00
Max Paulus 🥪 87543e6f47 Persist OpenRouter provider config via catalog hook 2026-06-24 14:11:27 +09:00
Max Paulus 🥪 3757a8fd0d persist openai-codex provider model settings
- also don't show a sign in button if openai-codex is the only provider
2026-06-24 14:11:27 +09:00
Max Paulus 🥪 0e9a5c0fc1 Persist Cline model selections to provider config 2026-06-24 14:11:27 +09:00
Dominic Cooney 8aba515d6b fix(vscode): declare missing direct deps @grpc/proto-loader and @opentelemetry/api-logs
Both packages are imported directly from source but were never declared in
apps/vscode/package.json, so they only resolved transitively. On a clean
install this broke:

- @grpc/proto-loader — imported by scripts/proto-utils.mjs,
  src/standalone/utils.ts and src/standalone/hostbridge-client.ts; its absence
  made `npm run protos` (and therefore the whole build) fail on a fresh checkout.
- @opentelemetry/api-logs — imported by the OpenTelemetry telemetry providers;
  its absence produced TS2307 "Cannot find module" errors under tsc.

Versions are pinned to align with the existing dependency families already
declared in this package (@grpc/grpc-js ^1.9.x → proto-loader ^0.7.13;
the @opentelemetry/* 0.56.x line → api-logs ^0.56.0). The npm and bun
lockfiles are updated accordingly (the api-logs change also dedupes several
previously-nested copies to a single hoisted entry).
2026-06-24 14:11:27 +09:00
Max Paulus 🥪 a2194f4908 show legacy task history that is not saved in the ~/.cline folder 2026-06-24 14:11:26 +09:00
Max Paulus 🥪 f5d0b4fd49 add migration telemetry 2026-06-24 14:11:26 +09:00
Ara 61ea8688c6 fix(vscode): reuse approved tool rows (#11213)
* fix(vscode): reuse approved tool rows

* fix(vscode): clear stale approved tool rows
2026-06-24 14:11:26 +09:00
Dominic Cooney 94f5a47a59 sdk migration: squashed pre-2026-06-02 work
Omnibus squash of the 10 oldest SDK-migration commits (authored 2026-05-27
through 2026-06-02), collapsed during the 2026-06-09 rebase onto origin/main.

Squashed commits:
- sdk migration: squashed pre-2026-05-27 work
- sdk migration: squashed 06-05-2026 -- instead of listHistory, use host.get(sessionId) instead
- updat gitignore
- fix xai provider
- fix(vscode): forward Bedrock region + AWS auth to the SDK gateway
- fix(vscode): keep in-progress MCP OAuth flow across reconnects
- fix(vscode): wire auto compact into SDK sessions (#11197)
- fix(vscode): compact Codex OAuth before input cap (#11194)
- fix unauthed user flow
- fix(llms): strip Cerebras reasoning history (#11214)
2026-06-24 14:11:26 +09:00
2373 changed files with 351598 additions and 220123 deletions
+1
View File
@@ -0,0 +1 @@
../../.cline/skills/publish-desktop
+1
View File
@@ -0,0 +1 @@
../../.cline/skills/publish-extension
+1
View File
@@ -0,0 +1 @@
../../.cline/skills/tuistory
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Remove the non-functional "Use compact prompt" toggle from LM Studio provider settings
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: restore workflow support regressions — expand `/workflow.md` slash commands (the legacy filename spelling the autocomplete inserts) and mid-message commands, honor workflow enable/disable toggles during expansion, refresh the slash menu's workflow list on webview launch, and bring back the Workflows management tab in the rules modal (now last in the tab list, with a deprecation notice pointing to Skills)
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix hidden plan/act mode-switch and task-resumption prompts reappearing as user messages when a task is reopened from history
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: strip trailing slashes from the OpenAI Compatible base URL when fetching the model list, so `/models` is queried correctly and the model dropdown populates
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: center-align the sign-in verification code box shown after clicking "Sign in to Cline"
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Enable Auto Compact by default so long chats automatically compress conversation history instead of failing at the model context limit. It can be disabled in Settings → Features → "Auto Compact".
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Bring back a copy button on turn-final response rows, under a new subtle "Completed" / "Plan" header
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix /compact UX: clear the chat input as soon as the command is submitted, wrap the compaction divider row at narrow sidebar widths, and update the context-window header even when compacting a small conversation grows the estimated context
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Disable feature tips by default; they can be enabled in Settings → Features → "Feature Tips"
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Show the edited file in a regular editor tab after the diff preview closes, restoring the legacy post-edit behavior
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Hide the "View Changes" button on completion rows until there are actually changes to show, instead of rendering it faded and disabled. Turns that changed nothing, non-git workspaces, and repos without commits no longer show a dead button with a misleading tooltip.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Show the user's message in chat immediately when sending to a task opened from history, instead of only a thinking indicator until the session resume finishes
+2 -2
View File
@@ -41,11 +41,11 @@ fi
# Install project dependencies
echo "Installing dependencies..."
npm run install:all
bun run install:all
# Generate gRPC/protobuf types (required for TypeScript)
echo "Generating proto types..."
npm run protos
bun run protos
echo ""
echo "Session setup complete!"
+1
View File
@@ -0,0 +1 @@
../../.cline/skills/publish-desktop
+1
View File
@@ -0,0 +1 @@
../../.cline/skills/publish-extension
+1
View File
@@ -0,0 +1 @@
../../.cline/skills/tuistory
+157
View File
@@ -0,0 +1,157 @@
---
name: publish-desktop
description: Use when preparing, tagging, and publishing a Cline Code desktop app (apps/examples/desktop-app) release. Guides changelog drafting, version bumps in package.json + tauri.conf.json, desktop-vX.Y.Z tags, and the desktop-publish GitHub workflow that builds, signs, notarizes, and updates the auto-update feed.
---
# Desktop App Release
Use this skill when the user asks to release the desktop app, publish Cline Code, bump the desktop version, create a `desktop-vX.Y.Z` tag, or trigger the desktop publish workflow.
> Working directory: run every command below from the repository root.
Desktop releases are macOS-only today (a single signed + notarized universal DMG that runs natively on both Apple Silicon and Intel) and are built entirely in GitHub Actions — there is no local publish path. Installed apps discover new releases automatically through the Tauri updater, so publishing a release is what ships the update to every existing user.
## Release contract
- Version sources (must match each other and the tag): `apps/examples/desktop-app/package.json` and `apps/examples/desktop-app/src-tauri/tauri.conf.json`. (`src-tauri/Cargo.toml` has its own version but `tauri.conf.json` overrides it; no need to touch it.)
- Release tag: `desktop-vX.Y.Z`, where `X.Y.Z` matches both version files.
- Release prep includes approved release notes, the version bumps, and an `apps/examples/desktop-app/CHANGELOG.md` update.
- Publish path: `.github/workflows/desktop-publish.yml` (workflow_dispatch, requires the tag to exist, point at the checked-out commit, and be reachable from `origin/main`).
- The workflow creates the `desktop-vX.Y.Z` GitHub release (universal DMG + updater artifact + `latest.json`) and refreshes the rolling `desktop-latest` release, which is the static auto-update feed every installed app polls. Never delete the `desktop-latest` release or tag.
- The changelog's top `## X.Y.Z` section is extracted verbatim into the GitHub release body, the Slack announcement, and the updater manifest notes.
- Always ask before pushing commits or tags.
## Workflow
1. Gather context.
```sh
git status --short --branch
git fetch origin --tags
git tag --list 'desktop-v*' --sort=-v:refname | head -10
node -p "require('./apps/examples/desktop-app/package.json').version"
node -p "require('./apps/examples/desktop-app/src-tauri/tauri.conf.json').version"
```
If there is no `desktop-v*` tag yet, this is the first release; use the desktop app's first commit as the baseline and say the baseline is inferred.
2. Collect release commits.
```sh
git log <last-desktop-tag>..HEAD --oneline --no-merges -- apps/examples/desktop-app sdk/packages .github/workflows/desktop-publish.yml
```
The sidecar bundles `@cline/core` and friends from the monorepo, so SDK changes ship inside the desktop app too. Fold user-visible SDK changes (providers, models, behavior fixes) into the notes; skip purely internal ones.
3. Draft user-facing release notes.
Flat bullet list, user-facing language. Present the draft and wait for approval before editing files.
4. Decide the version bump.
Ask whether this is patch, minor, major, or an explicit version. Do not guess if the user has not made it clear.
5. Update release files.
- `apps/examples/desktop-app/package.json` → new version
- `apps/examples/desktop-app/src-tauri/tauri.conf.json` → same version
- Prepend `## X.Y.Z` (no date) to `apps/examples/desktop-app/CHANGELOG.md` with the approved notes.
6. Verify before committing.
```sh
bun -F @cline/code typecheck
bun test apps/examples/desktop-app/scripts/generate-update-manifest.test.ts
```
The full desktop bundle can only be built on macOS; the workflow's build job is the real verification. For extra local confidence on a Mac checkout, `bun run package:desktop:mac --allow-unsigned-mac` from the app directory.
7. Commit release changes.
```sh
git add apps/examples/desktop-app/package.json apps/examples/desktop-app/src-tauri/tauri.conf.json apps/examples/desktop-app/CHANGELOG.md
git commit -m "chore(desktop): release vX.Y.Z"
```
Ask before pushing the release commit, then before creating and pushing the tag:
```sh
git push origin HEAD
git tag -a desktop-vX.Y.Z -m "Desktop vX.Y.Z"
git push origin refs/tags/desktop-vX.Y.Z
```
8. Publish.
The release commit must be on `main` and the tag pushed first.
```sh
gh workflow run desktop-publish.yml -f git_tag=desktop-vX.Y.Z -f confirm_publish=publish
gh run list --workflow=desktop-publish.yml --limit=1 --json url,status,conclusion,createdAt --jq '.[0]'
```
**The run pauses for approval.** `validate` runs immediately, then the `build`
job waits on the `PublishDesktop` environment until a required reviewer approves
it — the run sits in `waiting`, which is expected, not a hang. Approve it in the
run's web UI ("Review deployments"), or:
```sh
gh api repos/cline/cline/actions/runs/<run-id>/pending_deployments \
--method POST -f state=approved -f comment="desktop vX.Y.Z" \
-F 'environment_ids[]=19152605990' # PublishDesktop
```
Nothing after `validate` runs — and no signing key is readable — until then.
The workflow builds one universal macOS bundle (`tauri build --target universal-apple-darwin` lipos the aarch64 + x86_64 Rust binaries; the Bun sidecar is lipo'd by `build-sidecar-bin.ts`), verifies every Mach-O in the bundle carries both slices, signs with the Developer ID certificate, notarizes with the App Store Connect API key, signs the updater artifact with the Tauri updater key, creates the GitHub release, refreshes `desktop-latest/latest.json`, and posts to Slack. Notarization typically adds 210 minutes.
If the workflow fails on missing credentials, see "Publish secrets (one-time setup)" below.
9. Verify the update feed after the run succeeds.
```sh
curl -sL https://github.com/cline/cline/releases/download/desktop-latest/latest.json | head -30
```
The `version` field must be the new release and both `darwin-aarch64` and `darwin-x86_64` entries must point at the same new `desktop-vX.Y.Z` universal `.app.tar.gz` asset (each slice of the fat binary requests its own arch key at runtime, so both keys serve the one artifact). Installed apps — including older per-arch installs — pick the update up on next launch or within 2 hours.
10. Final response.
Report: version, tag, changelog updated, commit hash, what was pushed, workflow URL, and the feed verification result.
## Publish secrets (one-time setup)
These live on the **`PublishDesktop` environment**, not at repository level, so
only the `build` job can read them and only after an approval. Set them under
Settings → Environments → PublishDesktop → Environment secrets. The environment
also restricts deployments to `main` and requires a reviewer.
Adding one of these as a *repository* secret is the common mistake. The build
would still succeed — an environment-gated job resolves repository secrets too,
with environment values simply taking precedence — so the credential would sit
repo-wide while everything looked fine. `validate` therefore fails the run if any
of them resolves in a job with no environment. If you hit that, delete the
repository-level copy rather than duplicating it.
If a secret is missing everywhere, the preflight in `build` fails the run naming
the missing entries. The Apple values come from the same Apple Developer account
used for manual signing (see the app README's "macOS signing & notarization"
section for how to obtain them):
| Secret | Value |
| --- | --- |
| `APPLE_CERTIFICATE` | Base64 of the **Developer ID Application** identity exported from Keychain Access as `.p12` (must include the private key): `base64 -i certificate.p12 \| pbcopy` |
| `APPLE_CERTIFICATE_PASSWORD` | The password chosen when exporting the `.p12` |
| `APPLE_SIGNING_IDENTITY` | `Developer ID Application: <Team Name> (<TEAMID>)` — from `security find-identity -v -p codesigning` |
| `APPLE_API_KEY` | App Store Connect API **Key ID** (notarization) |
| `APPLE_API_KEY_CONTENT` | Contents of the `AuthKey_<KEYID>.p8` file |
| `APPLE_API_ISSUER` | App Store Connect **Issuer ID** (UUID from Users and Access → Integrations) |
| `TAURI_SIGNING_PRIVATE_KEY` | Contents of the Tauri updater private key (`tauri signer generate`). If this key is ever lost, shipped apps can no longer verify updates — guard it. |
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Password for that key |
The Slack + telemetry secrets (`SLACK_RELEASE_BOT_TOKEN`, `TELEMETRY_SERVICE_API_KEY`,
`ERROR_SERVICE_API_KEY`, OTEL settings) are shared with the CLI, SDK, and extension
publish workflows and already configured. **Do not move these into
`PublishDesktop`** — scoping them to this environment empties them in every other
publish workflow, silently, with no error beyond missing telemetry and a failed
Slack post.
+182
View File
@@ -0,0 +1,182 @@
---
name: publish-extension
description: Use when releasing the Cline VS Code extension — stable (currently the combined legacy+next A/B VSIX via ext-vscode-ab-package), nightly (ext-vscode-publish-nightly), or a legacy-branch hotfix (ext-vscode-publish-legacy). Guides version selection, changelog, PostHog rollout-flag coordination, workflow dispatch, environment approvals, tagging, and post-publish verification, plus the eventual cutover to publishing the SDK extension standalone.
---
# VS Code Extension Release
Use this skill when the user asks to release, publish, or ship the VS Code extension — stable, nightly, or a legacy hotfix — or to dial the rollout, or to cut over to the SDK extension permanently.
> Working directory: repo root. All workflows are dispatched from `main` (GitHub requires the workflow file on the default branch; each workflow checks out the refs it actually builds).
## The current era: combined A/B rollout
We are mid-migration from the legacy (npm, pre-SDK) extension to the next (SDK-based, bun) extension. Until the cutover is complete, **the stable and nightly listings ship a combined VSIX**: a small loader + two complete extensions (`next/` built from `main`, `legacy/` built from the `legacy-extension` branch). The loader picks one per window based on the PostHog flag `ext-sdk-bundle-rollout`. Deep-dive docs: `apps/vscode-rollout/README.md` (authoritative) and PR #12253 (design + runbook comments).
Endgame (see "Cutover" at the bottom): once the next bundle is trusted at 100%, stable goes back to a plain build of `main` via `ext-vscode-publish-stable.yml` and all the legacy/rollout machinery is retired.
### The listings and the workflows
| Channel | Marketplace ID | Workflow | Trigger | Version |
|---|---|---|---|---|
| Stable (combined) | `saoudrizwan.claude-dev` | `ext-vscode-ab-package.yml` | dispatch only; `publish` input defaults false | manual input (semver, e.g. `4.1.0`) |
| Nightly (combined) | `saoudrizwan.cline-nightly` | `ext-vscode-publish-nightly.yml` | cron 12:00 UTC + dispatch | auto `<major>.<minor>.<unix-ts>` from main's `apps/vscode/package.json` |
| Legacy hotfix (standalone) | `saoudrizwan.claude-dev` | `ext-vscode-publish-legacy.yml` | dispatch | from `apps/vscode/package.json` on `legacy-extension` |
| Stable standalone (post-cutover) | `saoudrizwan.claude-dev` | `ext-vscode-publish-stable.yml` | dispatch | from `apps/vscode/package.json` on `main` |
All three publish paths gate on tests before publishing: nightly and ab-package run the reusable bun suite (`ext-vscode-test.yml`, tests `main`) — ab-package additionally runs the legacy branch's npm suite — and the legacy workflow inlines the npm suite. Environment gates: stable paths use `publish``Publish` environment (required reviewers approve in the Actions UI); nightly uses `PublishNightly` (branch policy only, no reviewers — a reviewer requirement would block the cron).
## Golden rules (read before any release)
1. **One listing, one version line.** `claude-dev` is published from multiple workflows/branches. Every stable publish must use a version **strictly above the highest version ever published to the listing from any branch** — marketplace versions are monotonic and cannot be unpublished (supersede, never delete). Check what's live first:
```bash
curl -s -X POST "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery" \
-H "Content-Type: application/json" -H "Accept: application/json;api-version=3.0-preview.1" \
-d '{"filters":[{"criteria":[{"filterType":7,"value":"saoudrizwan.claude-dev"}]}],"flags":16}' \
| python3 -c "import json,sys; v=json.load(sys.stdin)['results'][0]['extensions'][0]['versions'][0]; print(v['version'], v['lastUpdated'])"
```
`ext-vscode-ab-package` also enforces this automatically for `publish=true` runs: a preflight job validates the version format (plain `X.Y.Z`) and hard-fails unless it exceeds the live Marketplace version, and the publish job re-checks right before publishing (the approval wait can last days — a legacy hotfix landing in between is caught). Still run the query yourself when *choosing* the version.
2. **Check the flag BEFORE any stable combined publish.** `ext-sdk-bundle-rollout` is **shared between nightly and stable** — the loader sends only a machine id to `/decide`, no channel property, so there is no per-channel targeting. If the flag is high (nightly dogfooding) and you publish stable, stable users get the next bundle at that same percentage. Verify the effective percentage empirically (no PostHog admin needed — sample `/decide` with random ids using the key inlined in any shipped loader):
```bash
node -e '
const KEY = process.argv[1]; // phc_... extracted from a shipped VSIX loader
(async () => {
let t = 0, n = 200;
for (let i = 0; i < n; i += 20) {
const rs = await Promise.all(Array.from({length: 20}, (_, j) =>
fetch("https://data.cline.bot/decide?v=3", { method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({api_key: KEY, distinct_id: `probe-${i+j}-${Math.random()}`})
}).then(r => r.json())));
for (const r of rs) if ((r.featureFlags||{})["ext-sdk-bundle-rollout"] === true) t++;
}
console.log(`~${(100*t/n).toFixed(1)}% (${t}/${n})`);
})()' "$KEY"
```
Flag changes are made in the PostHog UI (Cline project). **0% is the kill switch** — the flag is two-way; there is no separate killswitch flag. Dialing down demotes machines back to legacy on their next window reload.
3. **Ask before pushing** commits or tags. Environment approvals are the maintainer's to give.
4. **Changelog lives at the repo ROOT** (`CHANGELOG.md`), on the branch being released — not `apps/vscode/CHANGELOG.md` (doesn't exist). The legacy and stable workflows hard-fail unless the first heading is exactly `## [<version>]`.
5. **Stuck concurrency groups**: `ext-vscode-ab-package` groups on the version with `cancel-in-progress: false`. Only `publish=true` runs wait on environment approval (build-only rehearsals run ungated to completion), but a publish run left `waiting` still blocks every later dispatch of the same version — cancel it (`gh run cancel <id>`) before re-dispatching.
## Stable release (combined A/B VSIX) — the current stable path
### Pre-flight
```bash
# 1. What's live, and what version comes next (must exceed it — rule 1)
# 2. Flag percentage (rule 2) — decide where it should be for this release
# 3. Legacy tip = what the non-promoted cohort will run; confirm it's the shipped hotfix line
git fetch origin main legacy-extension
git log --oneline -3 origin/legacy-extension
# 4. Cheap local rehearsal of the most likely build failure: the union manifest
# hard-fails if views/viewsContainers/configuration diverged between branches.
git show origin/main:apps/vscode/package.json > /tmp/next.json
git show origin/legacy-extension:apps/vscode/package.json > /tmp/legacy.json
node apps/vscode-rollout/scripts/gen-manifest.mjs --next /tmp/next.json --legacy /tmp/legacy.json --version <VERSION>
# Expected warnings only: engines union (takes newer) + walkthrough copy drift.
```
Release prep on `main` (PR, not direct push):
- Add `## [<VERSION>]` entry at the top of root `CHANGELOG.md`.
- Bump `apps/vscode/package.json` to `<VERSION>` so the repo reflects the published line. Side effect: nightly versions become `<major>.<minor>.<unix-ts>` of the new base — harmless (separate listing, still monotonic).
### Dispatch
```bash
gh workflow run ext-vscode-ab-package.yml --ref main \
-f version=<VERSION> -f next-ref=main -f legacy-ref=legacy-extension -f publish=true
# publish=false builds an installable .vsix artifact without publishing and
# needs NO environment approval — the ungated build job uploads the artifact
# and the run completes.
gh run list --workflow=ext-vscode-ab-package.yml --limit 1
```
Preflight (version format + monotonicity) and both test suites run first, then the ungated `build` job packages and uploads the VSIX; for `publish=true` the `publish` job then **waits for `Publish` environment approval** (Actions → run → "Review deployments"). Both bundles build the exact revisions their test gates ran against (branch names are resolved once — commits landing on either branch mid-run or during the approval wait are not picked up); `publish=true` is additionally refused for any `next-ref` other than `main` (the bun gate only tests main — non-main next-refs are for build-only artifact rehearsals). Check what a run is waiting on:
```bash
gh api repos/cline/cline/actions/runs/<run-id>/pending_deployments
```
### Post-publish
1. Verify the marketplace serves the new version (query from rule 1) — expect minutes-to-an-hour of validation lag after "Published" appears in the logs. Also verify Open VSX:
```bash
curl -s "https://open-vsx.org/api/saoudrizwan/claude-dev" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['version'], d['timestamp'])"
```
2. Tag, GitHub Release (with the .vsix attached), and the Slack release-bot post happen **automatically** after a real publish (all `continue-on-error` — the publish itself already succeeded, so bookkeeping failures leave the run green). Verify they landed; the known failure is the tag push when the built commit touches `.github/workflows/**` (default token cannot create such refs — no grantable permission fixes it). Manual fallback:
```bash
git tag v<VERSION> <main-sha-built> # ask before pushing
git push origin v<VERSION>
gh release create v<VERSION> --title "v<VERSION>" --notes "<changelog section>" <path-to.vsix>
```
A real publish also **hard-fails early** if root `CHANGELOG.md` on the built main revision doesn't start with `## [<VERSION>]` — the release prep PR must be merged before dispatching.
3. Thorough artifact check (`gh run download <run-id>`): union `package.json` is `saoudrizwan.claude-dev@<VERSION>`, `next/package.json` and `legacy/package.json` carry the SAME version, `grep -c 'phc_' extension/extension.js` ≥ 1 (loader key inlined), no leftover `process.env.TELEMETRY_SERVICE_API_KEY` / `process.env.CLINE_ROLLOUT_VARIANT` literals in either bundle's dist (leftovers = a build ran without its env and telemetry is silently dead).
4. Monitor: `extension.rollout.bundle_activated` in `otel.otel_logs` filtered to `extension_version = '<VERSION>'` (stable cohort is cleanly separable — nightly versions are timestamps). Watch the next/legacy ratio and the crash-fallback rate; Metabase dashboards 17 (rollout + task error rate) and 19 (error deep dive). `extension.rollout.loader_decision` (incl. `double_failure`) is PostHog-only, not in ClickHouse.
5. Dial the flag per the rollout plan (e.g. 0% at publish → 1% → up), verifying each change with the probe from rule 2. Announce demotions ahead of time — dialing down also demotes nightly dogfooders unless they set `"cline-nightly.rollout.bundleOverride": "next"`.
### Known caveats of this path
- **`engines.vscode` unions upward** (main's floor wins, e.g. `^1.101.0` vs legacy's `^1.84.0`): users on older VS Code are never offered the combined VSIX. Fail-safe during rollout; must be resolved before 100%.
- A red run can still mean a successful publish on paths that tag (see Gotchas).
## Nightly release
Happens automatically (cron 12:00 UTC). Manual cut:
```bash
gh workflow run ext-vscode-publish-nightly.yml --ref main # real publish
gh workflow run ext-vscode-publish-nightly.yml --ref main -f dry-run=true # artifact only
gh run watch <run-id> --exit-status --interval 60
```
No changelog/version prep — the version is computed. Verify with the marketplace query against `saoudrizwan.cline-nightly`.
**Red run ≠ failed publish**: the final tag-push step fails whenever main's HEAD touches `.github/workflows/**` (default token cannot create such refs). If "Published" appears in the logs, the release went out; push the `nightly-main-<UTC ts>-<sha12>` tag manually with user credentials.
## Legacy hotfix release (and emergency full rollback)
For shipping a fix on the `legacy-extension` branch — or as the **structural rollback** from a bad combined stable VSIX: a standalone legacy publish at a higher version supersedes the combined VSIX entirely (loader and all) for every user. (For "next bundle misbehaving" you don't need this — dial the flag to 0% instead.)
```bash
# On legacy-extension: commit the fix, bump apps/vscode/package.json ABOVE the
# highest version ever published to the listing (rule 1 — including combined
# versions, e.g. combined 4.1.0 live -> hotfix is 4.1.1, not 4.0.13),
# add the matching `## [x.y.z]` entry to root CHANGELOG.md, push.
gh workflow run ext-vscode-publish-legacy.yml --ref main \
-f release-type=release -f branch=legacy-extension
```
npm test suite runs ungated; the publish job waits on the `Publish` environment. This workflow derives + pushes the `v<version>` tag itself and creates the GitHub release — no manual tagging. Publishes to Marketplace **and** Open VSX. The branch is the npm codebase: use `npm`, never `bun`, and expect the old monolith layout (`apps/vscode/src/core/...`).
## Cutover: retiring the A/B machinery (the endgame)
When the next bundle has held at 100% long enough to trust:
1. **Resolve the engines floor**: decide whether stranding VS Code < main's `engines.vscode` on the last combined version is acceptable, or lower main's floor first.
2. Bump `apps/vscode/package.json` on `main` above everything ever published; root `CHANGELOG.md` entry to match (both are enforced by the workflow).
3. Ship standalone from main: `gh workflow run ext-vscode-publish-stable.yml --ref main` — tests main, tags `v<version>` itself, creates the GitHub release, publishes Marketplace + Open VSX.
4. Watch the same rollout telemetry through the transition — `extension_variant` disappears from events as users leave combined builds, which is itself the adoption signal.
5. Only after the standalone version dominates: retire `legacy-extension` (keep for history), delete `ext-vscode-publish-legacy.yml` and `ext-vscode-ab-package.yml`, convert the nightly workflow back to a plain build of main, remove `apps/vscode-rollout/`, and archive the `ext-sdk-bundle-rollout` flag in PostHog (harmless to machines still on a combined VSIX: absent flag fails safe to... nothing changing until they update, but their loader treats a deleted flag as legacy — leave the flag at 100% until combined-VSIX activations flatline, then archive).
6. Update this skill: delete the combined-era sections and keep the standalone flow.
## Gotchas index
- `inputs.*` are empty strings on `schedule` events — preserve `|| 'default'` fallbacks when editing the nightly workflow.
- `bun run package` in `apps/vscode` does not build `@cline/*` workspace deps — fresh checkouts need `bun run build:sdk` first (workflows handle this).
- Job-level `if:` ref checks in workflow YAML are advisory (a dispatched branch runs its own copy of the file); the enforced boundary is each environment's deployment-branch policy in repo settings.
- Marketplace PATs (`VSCE_PAT`/`OVSX_PAT`) are only mounted into publish steps; neither publish workflow has an untrusted trigger surface.
- Environment-approval runs left waiting don't time out quickly — they sit for days and (for ab-package publish runs) block their version's concurrency group.
- Local forcing for manual testing: `CLINE_BUNDLE_OVERRIDE=next|legacy` env (launch VS Code fresh from a terminal) or the `<prefix>.rollout.bundleOverride` setting + reload; both report as `override` in telemetry so they don't pollute cohort data.
+158
View File
@@ -0,0 +1,158 @@
---
name: publish-ui
description: Prepare, validate, and publish standalone @cline/ui npm releases. Use when bumping the UI package version, publishing latest or next through ui-publish.yml, checking UI release readiness, or completing the one-time npm trusted-publishing bootstrap.
---
# Publish UI
Release `@cline/ui` independently from the Cline SDK runtime packages.
## Release contract
- Version source: `sdk/packages/ui/package.json`.
- Workflow: `.github/workflows/ui-publish.yml`.
- The package keeps `internal: true` only to stay out of the SDK's shared
version/publish scripts. It is still a public npm package because
`private: false` and `publishConfig.access: public` control npm publication.
- `latest` is the production channel. `next` is an opt-in preview channel.
- Use prerelease versions such as `0.2.0-next.0` for `next`; do not publish a
version intended for `latest` under the preview tag because npm versions
cannot be republished.
- There is no UI Git tag, GitHub release, schedule, or Slack announcement.
- The workflow runs only by manual dispatch. Every release attempt runs the UI
quality checks before publishing and requires `confirm_publish=publish` from
`main`.
- The publish job and npm trust relationship use the protected `Publish`
environment.
- Every npm publication needs a new semver version; npm versions are immutable.
- Always ask before pushing commits, triggering the publish workflow, changing
npm trust settings, or running a local publish command.
## Normal release
1. Inspect the branch, current version, npm state, and UI changes.
```sh
git status --short --branch
node -p "require('./sdk/packages/ui/package.json').version"
npm view @cline/ui dist-tags versions --json
git log --oneline --no-merges -- \
sdk/packages/ui apps/examples/desktop-app/webview/components/views/chat \
.github/workflows/ui-publish.yml
```
2. Ask for the npm channel and version together. For `latest`, ask for patch,
minor, major, or an explicit version. For `next`, require an explicit
prerelease version such as `0.2.0-next.0`. Do not guess. Update only
`sdk/packages/ui/package.json` and its workspace version in `bun.lock`. Do
not run the SDK version command.
3. Validate the release candidate.
```sh
bun install --filter @cline/ui --filter @cline/code --frozen-lockfile
bun -F @cline/ui typecheck
bun -F @cline/ui test
bun -F @cline/ui test:package
bun -F @cline/ui build-storybook
bun -F @cline/code test:chat-ui
```
The packed-package test installs the tarball with Bun/React 19 and with
npm/Node/React 18.
Inspect `bun pm pack --dry-run` when the exported file set changed.
4. Commit the version bump separately from feature work. Ask before pushing.
```sh
git add sdk/packages/ui/package.json bun.lock
git commit -m "chore(ui): release vX.Y.Z"
git push origin HEAD
```
5. After the release commit reaches `main`, restate the selected npm tag and ask
for explicit publish approval. Then trigger and watch the standalone
workflow:
```sh
run_url=$(gh workflow run ui-publish.yml --ref main \
-f npm_tag=latest \
-f confirm_publish=publish)
test -n "$run_url"
run_id=${run_url##*/}
gh run watch "$run_id" --exit-status
```
Use `npm_tag=next` only for a deliberate preview. Do not report success until
the workflow succeeds and npm shows the exact version under the selected tag.
```sh
npm view @cline/ui dist-tags versions --json
```
## One-time npm bootstrap
Use this only while `npm view @cline/ui` returns `E404`. npm requires the
package to exist before its GitHub trusted publisher can be configured.
1. Merge the package and `ui-publish.yml` to `main`. Start from a clean,
reviewed `main` checkout. Verify authentication, account 2FA, and write
access to the `@cline` npm organization. The `npm trust` command in step 4
requires npm CLI 11.15 or newer; the automated trusted-publishing workflow
itself enforces npm 11.5.1 or newer.
```sh
npm --version
npm whoami
npm view @cline/ui version
```
If npm is older than 11.15, ask before upgrading with
`npm install -g npm@^11.15.0`.
2. Run the normal release validation in step 3 above. Then build, pack, test,
and inspect the exact initial tarball. Record the absolute archive path
printed by the final command.
```sh
bun -F @cline/ui build
pack_dir=$(mktemp -d)
(cd sdk/packages/ui && bun pm pack --ignore-scripts --destination "$pack_dir" --quiet)
tarball=$(find "$pack_dir" -maxdepth 1 -name '*.tgz' -print -quit)
test -n "$tarball"
bun sdk/packages/ui/scripts/smoke-package.ts "$tarball"
tar -tzf "$tarball"
printf 'Bootstrap archive: %s\n' "$tarball"
```
3. Ask for explicit approval, then publish the initial version publicly under
`latest`:
```sh
npm publish /absolute/path/from-step-2.tgz --access public --tag latest
```
4. Ask separately before configuring the standalone workflow as the trusted
publisher:
```sh
npm trust github @cline/ui \
--repo cline/cline \
--file ui-publish.yml \
--env Publish \
--allow-publish
```
5. Verify both package state and trust. Every later release uses the workflow;
do not add a long-lived npm token.
```sh
npm view @cline/ui dist-tags versions --json
npm trust list @cline/ui
```
## Final report
Report the version and npm tag, release commit, whether anything was pushed,
workflow URL or bootstrap result, npm verification, and tests/builds run. If
the package still returns `E404`, state that bootstrap remains required.
@@ -0,0 +1,4 @@
interface:
display_name: "Publish UI"
short_description: "Prepare and publish the Cline UI package"
default_prompt: "Use $publish-ui to prepare and publish a new @cline/ui npm release."
+107
View File
@@ -0,0 +1,107 @@
---
name: tuistory
description: |
Drive and test terminal apps (especially the Cline CLI TUI in apps/cli) through tuistory — named background PTY sessions that agents can read, wait on, snapshot, screenshot, and type into. Like Playwright/tmux for terminals, with reactive waiting instead of blind `sleep`.
Use this skill when you need to:
- Manually test or reproduce bugs in the interactive Cline TUI (`bun run cli -i`) from a headless environment
- Run a dev server or any long-lived/interactive process in the background without hanging your tool call
- Write or extend Playwright-style e2e tests for the TUI (`bun run test:e2e:tuistory` in apps/cli)
- Capture text snapshots or styled PNG screenshots of a TUI screen as evidence
---
# tuistory
[tuistory](https://github.com/remorses/tuistory) wraps any terminal command in a named background PTY session backed by a Ghostty terminal emulator. Agents interact with the session via short CLI calls that return instantly; humans can `tuistory attach` to the same session to watch or intervene. No real terminal or display (`DISPLAY`) is needed — it works fully headless, which makes it the preferred way for cloud agents to exercise the Cline TUI.
It is installed as a devDependency of `@cline/cli`, so the pinned binary resolves when you run from `apps/cli`:
```bash
cd apps/cli
bunx tuistory --help # source of truth for commands, options, and syntax
```
For full upstream docs: `curl -s https://raw.githubusercontent.com/remorses/tuistory/refs/heads/main/README.md`
## Driving the Cline TUI headlessly
Launch the TUI in an isolated environment so you don't touch real user config (`~/.cline`):
```bash
cd apps/cli
DATA_DIR=$(mktemp -d) && HOME_DIR=$(mktemp -d)
bunx tuistory -s cline --cols 120 --rows 36 \
--env HOME=$HOME_DIR --env CLINE_DATA_DIR=$DATA_DIR \
--env CLINE_DISABLE_CLINE_PASS_NOTICE=1 --env CLINE_TELEMETRY_DISABLED=1 \
-- bun src/index.ts --provider anthropic -m claude-sonnet-4-6 -k test-key
```
The dummy `-k test-key` renders the full chat UI; only an actual agent turn would fail. For recorded LLM turns, use the VCR cassettes described in `apps/cli/src/tests/helpers/env.ts` (`CLINE_VCR=playback` + `CLINE_VCR_CASSETTE`). Real turns need a provider credential (e.g. `ANTHROPIC_API_KEY`, `CLINE_API_KEY`).
Then use an **observe → act → observe** loop:
```bash
# Wait reactively for the chat view — never use sleep
bunx tuistory -s cline wait "What can I do for you?" --timeout 30000
# Act, then always observe the resulting screen state
bunx tuistory -s cline type "/settings"
bunx tuistory -s cline snapshot --trim
bunx tuistory -s cline press enter
bunx tuistory -s cline snapshot --trim
# Styled PNG of the current screen (prints the file path) — good for artifacts
bunx tuistory -s cline screenshot
# Full raw output stream (snapshot shows only the visible screen)
bunx tuistory read -s cline --all
# Tear down a session YOU started (double Ctrl+C exits the TUI cleanly)
bunx tuistory -s cline press ctrl c
bunx tuistory -s cline press ctrl c
bunx tuistory -s cline close
```
## Background processes (instead of tmux)
```bash
bunx tuistory -s my-server -- bun run dev:sidecar # returns immediately
bunx tuistory -s my-server wait "/listening|ready/i" --timeout 30000
bunx tuistory read -s my-server # new output since last read
bunx tuistory -s my-server restart # after code changes
```
## Key rules
- **Options before `--`, command after.** Everything after the first `--` is passed verbatim to the child: `tuistory -s name --cols 150 -- bun src/index.ts` is correct.
- **Snapshot after every action.** TUIs are stateful; dialogs and errors can render over the view you expect. `snapshot` reflects what the user actually sees (occluded text does not count), unlike grepping the raw stream.
- **Wait, never sleep.** `wait "text"` / `wait "/regex/i"` (case-sensitive by default) reacts as fast as the terminal updates; `wait-idle` when you don't know what to expect. Always pass `--timeout`.
- **Keys land instantly.** Unlike sleep-based scripts, a queued second keypress can leak into the next view (e.g. one Enter both accepts a slash completion and submits it).
- **Never close a session you didn't start.** Sessions are shared with humans (`tuistory attach -s name`) and other agents. Default to leaving sessions running; use `read`/`wait`/`snapshot` to inspect without disrupting.
- `--cols`/`--rows` affect TUI layout (assertions are width-sensitive); `--pixel-ratio 2` gives sharper screenshots.
## Writing e2e tests with the library API
`apps/cli/src/cli.tuistory.e2e.test.ts` (run: `bun run test:e2e:tuistory`) is the reference. The programmatic API runs in-process — no daemon:
```ts
import { launchTerminal } from "tuistory";
const session = await launchTerminal({
command: "bun",
args: ["src/index.ts", "--provider", "anthropic", "-k", "test-key"],
cwd: cliRoot,
env: isolatedEnv, // see createCliEnv() in the reference test
cols: 120,
rows: 36,
waitForDataTimeout: 30_000, // CLI cold start compiles a large TS graph
});
await session.waitForText("What can I do for you?", { timeout: 30_000 });
const screen = await session.text({ trimEnd: true }); // emulated screen state
await session.type("/settings");
await session.press("enter");
session.close(); // always close in test teardown
```
Screen-state assertions can check that stale UI is *gone* (`expect(screen).not.toContain(...)`), which stream-grepping harnesses cannot. `session.text({ only: { bold: true } })` filters by style; `session.read()` returns the raw stream since the last read.
+55
View File
@@ -0,0 +1,55 @@
# Bun (tooling) and Node (runtime)
This repo uses **bun** for package management and task running, and **Node** as
the execution runtime. Both are correct at the same time; the distinction is the
source of most confusion, so keep it straight before editing scripts, configs,
docs, or comments.
## Use bun for tooling
- `bun install` (never `npm install` / `npm ci`)
- `bun run <script>` (never `npm run <script>`)
- `bunx <bin>` (never `npx <bin>`)
- `bun <file>.ts` to run a TS entrypoint directly (no `ts-node` / `tsx`)
- `bun esbuild.mjs` to drive the build (esbuild/vite are still the bundlers)
- `bun run --parallel ...` for parallel tasks
The root `bun.lock` is the single lockfile for the whole workspace, including
`apps/vscode`, `webview-ui`, and `testing-platform`. There are no per-package npm
lockfiles.
## Node is the runtime — do NOT rewrite these to bun
The build product runs on Node: the VS Code extension host loads
`dist/extension.js` as CommonJS under Node, and the standalone `cline-core` is a
Node process. The following are Node runtime/ABI references and are correct as-is:
| Reference | Why it is Node |
|-----------|----------------|
| esbuild `platform: "node"` / `target: "node..."` | The bundle targets the Node runtime (extension host, standalone core). |
| `TARGET_NODE_VERSION` (`scripts/package-standalone.mjs`) | Pins the Node ABI of the bundled standalone runtime (matches the JetBrains-packaged Node). |
| `prebuild-install --target=<node version>` | Downloads native `.node` binaries for that Node ABI. |
| `NODE_PATH=... node cline-core.js` | The standalone core is launched by Node, not bun. |
| `node:` import specifiers (e.g. `node:fs`) | Node builtin module scheme; unrelated to tooling. |
| `process.versions.node`, `engines.node`, `@types/node` | Runtime version probe / declared runtime / its types. |
| `ELECTRON_RUN_AS_NODE` | VS Code/Electron runs the extension host as Node. |
When a file legitimately uses both bun and node (e.g. `package-standalone.mjs`
does `bun install` but `prebuild-install --target=<node>`), the `node` token is
the runtime/ABI target, not tooling. If unsure, leave it.
## Tests: bun vs the VS Code host
A test file's runner is decided by its import:
- **`import ... from "bun:test"`** → runs under `bun test` (the node-side unit
suites + the SDK/model-catalog suites). `scripts/run-bun-unit-tests.ts`
discovers these by the `bun:test` import and runs one isolated bun process per
file. `build-tests.js` excludes them from the integration compile so the
`bun:test` builtin never reaches Node.
- **`import ... from "mocha"`** → runs under `@vscode/test-cli` in a real VS Code
extension host (Node). These exercise the live `vscode` API and cannot run
under bun.
So a file imports `bun:test` XOR `mocha`. Don't add `bun:test` to a test that
needs the real extension host.
+129
View File
@@ -0,0 +1,129 @@
# 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):
bun run protos && IS_DEV=true bun esbuild.mjs
# Launch (skip-build if already built). Run with node, NOT bun — Playwright's
# Electron launch times out under bun:
node 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.
`oauth.simulate_callback` only *builds* the URI — it does not deliver it, and the ESM
extension host can't `require()` the handler. To actually deliver the callback, call the
debug-only hook via `ext.evaluate` (with `awaitPromise: true`):
`globalThis.__clineHandleUri("vscode://saoudrizwan.claude-dev/...?code=...&state=...")`.
It runs the same `SharedUriHandler.handleUri` as VSCode's real URI handler and exists only
when `CLINE_CAPTURE_BROWSER` is set (the harness always sets it; never ships in prod).
For end-to-end MCP OAuth, get a real `code` from the local MCP OAuth test server
(`bun run dev:mcp-oauth-test-server`).
## 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.
+94 -90
View File
@@ -13,11 +13,57 @@ This file is the secret sauce for working effectively in this codebase. It captu
**What NOT to add:** Stuff you can figure out from reading a few files, obvious patterns, or standard practices. This file should be high-signal, not comprehensive.
## Miscellaneous
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
- The whole repo (including `apps/vscode`) uses **bun** for package management and task running. Emit `bun run X` / `bun install` / `bunx <bin>` / `bun file.ts`, never npm/npx. Node remains the *runtime* (VS Code's extension host and the standalone cline-core are Node), so Node-runtime tokens are legitimate and must not be "fixed" to bun — see @.clinerules/bun-and-node.md for the keep-list vs rewrite-list.
- Avoid provider-specific string matching / hardcoded provider branches when fixing provider/config plumbing. Prefer provider metadata, shared catalog/defaults, explicit protocol/client capabilities, or centralized normalization utilities that apply by data shape rather than `providerId === "..."`. If a provider exception seems necessary, stop and explain why instead of adding ad-hoc string matching.
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `bun run compile`, not `bun run build`).
- When reading a configuration files that users may edit, use `readFileStrippingUtf8Bom`, `readFileSyncStrippingUtf8Bom`, or `stripUtf8Bom` from `@cline/shared/node`. DON'T strip byte order marks of user files handled by tools/passed to models.
- When creating PRs, contributors should not create changelog-entry files. Maintainers handle release versioning and changelog curation during the release process.
- When adding new feature flags, see this PR as a reference https://github.com/cline/cline/pull/7566
- Additional instructions about making requests: @.clinerules/network.md
## 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.
@@ -28,7 +74,7 @@ The extension and webview communicate via gRPC-like protocol over VS Code messag
- Naming: Services `PascalCaseService`, RPCs `camelCase`, Messages `PascalCase`
- For streaming responses, use `stream` keyword (see `subscribeToAuthCallback` in `account.proto`)
**Run `npm run protos`** after any proto changes—generates types in:
**Run `bun run protos`** after any proto changes—generates types in:
- `src/shared/proto/` - Shared type definitions
- `src/generated/grpc-js/` - Service implementations
- `src/generated/nice-grpc/` - Promise-based clients
@@ -48,93 +94,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.
@@ -151,7 +110,7 @@ Settings plumbing gotcha: if a key is user-toggleable from settings, wire both c
Missing one path causes a toggle to appear to change in one surface while the backend state stays unchanged.
Webview toggle gotcha: settings changes must also round-trip back in state payloads.
- Add the field to `UpdateSettingsRequest` in `proto/cline/state.proto` (for webview update requests), then run `npm run protos`
- Add the field to `UpdateSettingsRequest` in `proto/cline/state.proto` (for webview update requests), then run `bun run protos`
- Include the key in `Controller.getStateToPostToWebview()` (`src/core/controller/index.ts`)
- Ensure `ExtensionState` and webview defaults include the key (`src/shared/ExtensionMessage.ts`, `webview-ui/src/context/ExtensionStateContext.tsx`)
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.
@@ -199,3 +158,48 @@ const isGenerating = explanationInfo.status === "generating" && !wasCancelled
**See also:** `BrowserSessionRow.tsx` uses similar pattern with `isLastApiReqInterrupted` and `isLastMessageResume`.
**Backend side:** When streaming is cancelled, clean up properly (close tabs, clear comments, etc.) by checking `taskState.abort` after the streaming function returns.
## Debug Harness: clear inherited VSCode/Electron env vars before launching
The debug harness (`apps/vscode/src/dev/debug-harness/server.ts`) launches a child
VSCode via Playwright's `_electron.launch({ env: { ...process.env, ... } })`. If you
run the harness from a process that was itself spawned by VSCode (e.g. the Cline
extension host, an integrated terminal, or an agent running inside VSCode), the
parent's VSCode/Electron env vars leak into the child and break the launch.
The fatal one is **`ELECTRON_RUN_AS_NODE=1`**: it makes the child VSCode binary run
as plain Node, so it rejects every VSCode CLI flag. Symptom:
```
.../Visual Studio Code.app/Contents/MacOS/Code: bad option: --extensionDevelopmentPath=...
Error: Process failed to launch! (Playwright _electron.launch)
```
This is NOT the macOS Playwright flakiness mentioned in the harness README — it's
env inheritance. Fix: strip the inherited vars before starting the harness:
```bash
env -u ELECTRON_RUN_AS_NODE -u ELECTRON_NO_ATTACH_CONSOLE \
-u VSCODE_CLI -u VSCODE_CODE_CACHE_PATH -u VSCODE_CRASH_REPORTER_PROCESS_TYPE \
-u VSCODE_CWD -u VSCODE_ESM_ENTRYPOINT -u VSCODE_HANDLES_UNCAUGHT_ERRORS \
-u VSCODE_IPC_HOOK -u VSCODE_NLS_CONFIG -u VSCODE_PID -u VSCODE_L10N_BUNDLE_LOCATION \
bun src/dev/debug-harness/server.ts --auto-launch --skip-build
```
Check your own env with `env | grep -iE 'electron|vscode_'` first; `ELECTRON_RUN_AS_NODE=1`
present means you must scrub before launching.
Other harness notes confirmed in practice:
- The extension host is **ESM** (`VSCODE_ESM_ENTRYPOINT`), so `ext.evaluate` has no
`require` and module-internal functions aren't reachable as globals. To inspect
internal builders (e.g. `buildBedrockProviderConfig`), set a breakpoint with
`ext.set_breakpoint` and read locals via `ext.evaluate` with the paused `callFrameId`
— don't try to `require()` the bundle.
- `web.evaluate` wraps the expression as a single returned expression; multi-statement
snippets must be an IIFE `(() => { ...; return x; })()`, otherwise you get
`SyntaxError: Unexpected token ';'`.
- Webview settings inputs are `vscode-text-field` web components with debounced React
onChange. Setting `.value` + dispatching events via `web.evaluate` is unreliable for
some fields; focus the inner shadow `input` then use real keystrokes (`ui.type` +
`ui.press Tab`, or click the dropdown option) to make the value persist.
+1 -1
View File
@@ -42,7 +42,7 @@ Here, we use the common `StringRequest` and `KeyValuePair` types.
After editing a `.proto` file, regenerate the TypeScript code. From the project root, run:
```bash
npm run protos
bun run protos
```
This command compiles all `.proto` files and outputs the generated code to `src/generated/` and `src/shared/`. Do not edit these generated files manually.
+26
View File
@@ -0,0 +1,26 @@
# SDK Adapter
The VSCode extension runs on the Cline SDK (`@cline/core`, `@cline/llms`,
`@cline/shared`) through an adapter layer in `apps/vscode/src/sdk/`. The
webview still talks gRPC; the adapter translates between gRPC handlers and SDK
calls. See `apps/vscode/src/dev/debug-harness/README.md` for the debug harness.
## Conventions
1. **Look up SDK APIs, don't guess.** Use `kb_search(name="sdk", query="...")`
before implementing against an SDK surface.
2. **Reference the pre-SDK implementation when replacing a module.** Add a
`// Replaces classic src/core/... (see origin/main)` header and use
`kb_search(name="cline", commit="origin/main")` or
`git show origin/main:path` to consult the prior implementation.
3. **Single entry point.** There is one codepath — the SDK adapter. No
`CLINE_SDK` env flag.
4. **Use `{appBaseUrl}`**, never hardcode `app.cline.bot`.
5. **Avoid `as` casts.** Use explicit conversion functions with tests. The
branded types in `apps/vscode/src/sdk/model-catalog/contracts.ts` exist so
casts are unnecessary outside parse/compute boundaries.
## Debug harness
- **Dismiss the Kanban/promo overlay** before any debug harness interaction.
- **Use the command palette** to navigate tabs in the debug harness.
+1 -1
View File
@@ -91,7 +91,7 @@ On the main branch, create a commit that updates:
3. No changelog-entry file cleanup is needed. Contributors do not create changelog-entry files in this repo.
**Skip running `npm run install:all`** - release automation handles lockfile consistency as needed.
**No dependency install is needed.** A CHANGELOG + `version` bump does not change any dependency, and `bun.lock` does not pin workspace-package versions, so the lockfile stays consistent. The publish workflow runs `bun install --frozen-lockfile`, which would *fail* on an out-of-sync lock — so only run `bun install` here if you actually change dependencies (then commit the updated `bun.lock`).
Commit with message format: `v{VERSION} Release Notes (hotfix)`
+6 -6
View File
@@ -9,14 +9,14 @@ This is a VS Code extension. Read `.clinerules/general.md` for tribal knowledge
- **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`.
- **Build**: `bun run compile` — NOT `bun run build`.
- **Watch**: `bun run watch` (extension + webview).
- **Protos**: `bun run protos` — run **immediately** after any `.proto` change. Generates into `src/shared/proto/`, `src/generated/`.
- **Tests**: `bun run test:unit`. After prompt/tool changes: `UPDATE_SNAPSHOTS=true bun 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`.
2. **Generate**: `bun 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`.
@@ -38,7 +38,7 @@ For Responses API providers: add to `isNextGenModelProvider()` in `src/utils/mod
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`.
7. Regenerate snapshots: `UPDATE_SNAPSHOTS=true bun 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.
+1 -1
View File
@@ -59,7 +59,7 @@ We're not looking for exhaustive documentation - just evidence that you've thoug
<!-- Put an 'x' in all boxes that apply -->
- [ ] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
- [ ] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`)
- [ ] Tests are passing (`bun test`) and code is formatted and linted (`bun run format && bun run lint`)
- [ ] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
### Screenshots
+455
View File
@@ -0,0 +1,455 @@
name: desktop-publish
on:
workflow_dispatch:
inputs:
git_tag:
description: "Existing release tag to publish, for example desktop-v0.1.0"
required: true
type: string
confirm_publish:
description: 'Type "publish" to confirm the desktop release.'
required: true
type: string
permissions:
contents: read
defaults:
run:
working-directory: .
jobs:
validate:
name: Validate release tag
if: |
github.repository == 'cline/cline' &&
github.event.inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
steps:
# Companion to the presence check in `build`, and the half that actually
# establishes scope. This job declares no environment, so a signing secret
# that resolves here can only be a repository or organization secret —
# meaning it is still readable by every workflow in the repo, which is the
# thing the PublishDesktop environment exists to prevent. Neither check
# proves provenance alone (an environment-gated job resolves repository
# secrets too, with environment values merely taking precedence), but
# together they do: empty here plus present in `build` means the value came
# from the environment.
- name: Verify signing secrets are not repository-scoped
env:
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
unscoped=()
for name in APPLE_API_ISSUER APPLE_API_KEY APPLE_API_KEY_CONTENT \
APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY \
TAURI_SIGNING_PRIVATE_KEY TAURI_SIGNING_PRIVATE_KEY_PASSWORD; do
[ -z "${!name}" ] || unscoped+=("$name")
done
if [ ${#unscoped[@]} -gt 0 ]; then
echo "These signing secrets resolve in a job with no environment:"
printf ' - %s\n' "${unscoped[@]}"
echo
echo "That means they are still repository or organization secrets and"
echo "are readable by any workflow in this repo. Delete them at that"
echo "level and add them to the PublishDesktop environment instead."
exit 1
fi
echo "No signing secret resolves outside the PublishDesktop environment."
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.git_tag }}
fetch-depth: 0
fetch-tags: true
- name: Validate release tag
id: version
env:
TAG: ${{ github.event.inputs.git_tag }}
run: |
if ! printf "%s\n" "$TAG" | grep -Eq '^desktop-v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "git_tag must look like desktop-vX.Y.Z, got: ${TAG}"
exit 1
fi
VERSION="${TAG#desktop-v}"
PACKAGE_VERSION=$(node -p "require('./apps/examples/desktop-app/package.json').version")
TAURI_VERSION=$(node -p "require('./apps/examples/desktop-app/src-tauri/tauri.conf.json').version")
if [ "$PACKAGE_VERSION" != "$VERSION" ]; then
echo "apps/examples/desktop-app/package.json version ${PACKAGE_VERSION} does not match ${TAG}"
exit 1
fi
if [ "$TAURI_VERSION" != "$VERSION" ]; then
echo "apps/examples/desktop-app/src-tauri/tauri.conf.json version ${TAURI_VERSION} does not match ${TAG}"
exit 1
fi
TAG_COMMIT=$(git rev-parse "${TAG}^{commit}")
HEAD_COMMIT=$(git rev-parse HEAD)
if [ "$TAG_COMMIT" != "$HEAD_COMMIT" ]; then
echo "${TAG} does not point at the checked out commit"
exit 1
fi
git fetch origin +main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "$HEAD_COMMIT" origin/main; then
echo "${TAG} is not reachable from origin/main"
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
build:
name: Build macOS (universal)
needs: validate
# The Apple signing/notarization and Tauri updater secrets live in the
# PublishDesktop environment rather than at repository level, so they are
# readable only by this job and only once a required reviewer approves the
# run. Defense in depth: this `if` is advisory because a dispatched branch
# runs its own copy of this file; the enforced gate is the PublishDesktop
# environment's deployment-branch policy, which must also allow only main.
if: github.ref == 'refs/heads/main'
environment: PublishDesktop
runs-on: macos-latest
timeout-minutes: 90
steps:
# A secret missing here is dangerous rather than merely broken: Tauri skips
# code signing when APPLE_CERTIFICATE is empty and skips notarization when
# APPLE_API_KEY is empty, both silently, so the build would still succeed
# and publish an unsigned, un-notarized bundle. Only the missing updater
# key is caught later (by the .sig check in "Collect artifacts"). Fail up
# front instead, before any build work, if the environment is misconfigured.
- name: Verify PublishDesktop secrets are present
env:
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
missing=()
for name in APPLE_API_ISSUER APPLE_API_KEY APPLE_API_KEY_CONTENT \
APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY \
TAURI_SIGNING_PRIVATE_KEY TAURI_SIGNING_PRIVATE_KEY_PASSWORD; do
[ -n "${!name}" ] || missing+=("$name")
done
if [ ${#missing[@]} -gt 0 ]; then
echo "Missing from the PublishDesktop environment:"
printf ' - %s\n' "${missing[@]}"
echo
echo "Check that every secret above is set on the PublishDesktop"
echo "environment and that this job still declares"
echo "'environment: PublishDesktop'."
exit 1
fi
# Deliberately not phrased as "resolved from PublishDesktop": a
# non-empty value here could also be a repository or organization
# secret. The repository-scope check in `validate` is what rules that
# out.
echo "All 8 signing secrets are present."
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.validate.outputs.tag }}
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
# A universal (fat) macOS bundle needs both architecture slices, so
# install both Rust targets; `tauri build --target universal-apple-darwin`
# compiles each and lipos the results into one binary.
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin
# No Rust build cache here, deliberately. This is the only job that can
# read the Apple signing certificate and the Tauri updater key, and a
# restored cache archive is attacker-controlled the moment the Actions
# cache is poisoned.
- name: Install dependencies
run: bun install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build SDK packages
run: bun run build:sdk
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
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 }}
- name: Write App Store Connect API key
env:
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
run: |
if [ -z "$APPLE_API_KEY_CONTENT" ]; then
echo "APPLE_API_KEY_CONTENT secret is not configured"
exit 1
fi
printf "%s" "$APPLE_API_KEY_CONTENT" > "$RUNNER_TEMP/AuthKey.p8"
- name: Build, sign, and notarize desktop bundle
working-directory: apps/examples/desktop-app
run: bunx tauri build --target universal-apple-darwin --config src-tauri/tauri.release.conf.json
env:
# Telemetry config for the sidecar binary. Tauri's beforeBuildCommand
# (`bun run build` -> build:sidecar:bin) compiles the sidecar during
# this step and inlines these values into the binary via `--define`
# (scripts/telemetry-define-args.ts); a packaged app launched from
# Finder/the Dock has no runtime env, so build-time inlining is the
# only way the shipped sidecar can ever report telemetry.
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
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 }}
# Developer ID signing (Tauri imports the cert into a temp keychain)
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
# Notarization via App Store Connect API key. Tauri reads the Key ID
# from APPLE_API_KEY; APPLE_API_KEY_ID alone silently skips notarization.
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: ${{ runner.temp }}/AuthKey.p8
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
# Updater artifact signing (minisign keypair, independent of Apple)
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
# Tauri lipos the main binary itself but sidecars are merged by our own
# build-sidecar-bin.ts, so assert every Mach-O in the bundle really
# carries both slices before anything is published. A single-arch
# sidecar would otherwise ship fine and only crash on the other arch.
- name: Verify bundle is a universal binary
working-directory: apps/examples/desktop-app
run: |
APP="src-tauri/target/universal-apple-darwin/release/bundle/macos/Cline Code.app"
if [ ! -d "$APP" ]; then
echo "app bundle not found at $APP"
exit 1
fi
for bin in "$APP/Contents/MacOS/"*; do
archs=$(lipo -archs "$bin")
echo "$bin: $archs"
case "$archs" in
*arm64*x86_64*|*x86_64*arm64*) ;;
*)
echo "$bin is not a universal binary (archs: $archs)"
exit 1
;;
esac
done
# Guardrail: assert the telemetry config actually made it into the
# compiled sidecar. Missing env on the build step (or a regression in
# the --define inlining) would otherwise ship a release with telemetry
# silently disabled — exactly what happened for every release before
# this check existed. Being enabled is not enough on its own: an empty,
# malformed, or non-http(s) OTLP endpoint would still drop every event
# at runtime (the SDK exporters speak OTLP http/json only), so the
# selfcheck must also report a usable endpoint host.
- name: Verify sidecar telemetry config was inlined
working-directory: apps/examples/desktop-app
run: |
SELFCHECK=$(./src-tauri/bin/code-sidecar-universal-apple-darwin --telemetry-selfcheck)
echo "$SELFCHECK"
if ! printf '%s' "$SELFCHECK" | grep -q '"enabled":true'; then
echo "Packaged sidecar reports telemetry disabled."
echo "Check the OTEL_* / TELEMETRY_SERVICE_API_KEY env on the"
echo "'Build, sign, and notarize desktop bundle' step and the"
echo "--define inlining in scripts/build-sidecar-bin.ts."
exit 1
fi
if printf '%s' "$SELFCHECK" | grep -Eq '"otlp_endpoint_host":"(invalid-endpoint-url)?"'; then
echo "Packaged sidecar reports telemetry enabled but its OTLP"
echo "endpoint is missing, unparseable, or not an http(s) URL, so"
echo "every event would be dropped at runtime. Check the"
echo "OTEL_EXPORTER_OTLP_ENDPOINT secret."
exit 1
fi
- name: Collect artifacts
working-directory: apps/examples/desktop-app
env:
VERSION: ${{ needs.validate.outputs.version }}
run: |
BUNDLE_DIR="src-tauri/target/universal-apple-darwin/release/bundle"
OUT="dist/publish"
mkdir -p "$OUT"
DMG=$(find "$BUNDLE_DIR/dmg" -name '*.dmg' -print -quit)
if [ -z "$DMG" ]; then
echo "no DMG produced under $BUNDLE_DIR/dmg"
exit 1
fi
cp "$DMG" "$OUT/Cline-Code_${VERSION}_universal.dmg"
TARBALL=$(find "$BUNDLE_DIR/macos" -name '*.app.tar.gz' -print -quit)
if [ -z "$TARBALL" ] || [ ! -f "${TARBALL}.sig" ]; then
echo "updater artifact or signature missing under $BUNDLE_DIR/macos"
exit 1
fi
cp "$TARBALL" "$OUT/Cline-Code_${VERSION}_universal.app.tar.gz"
cp "${TARBALL}.sig" "$OUT/Cline-Code_${VERSION}_universal.app.tar.gz.sig"
ls -lh "$OUT"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: desktop-universal
path: apps/examples/desktop-app/dist/publish/*
if-no-files-found: error
release:
name: Create GitHub release
needs: [validate, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.validate.outputs.tag }}
fetch-depth: 0
fetch-tags: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: dist/desktop
merge-multiple: true
- name: Get Changelog Entry
id: changelog
run: |
# Grab content between the first "## " header and the next one
CONTENT=$(awk '/^## [0-9]/{if(found) exit; found=1; next} found{print}' apps/examples/desktop-app/CHANGELOG.md)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
printf "%s\n" "$CONTENT" > "$RUNNER_TEMP/release-notes.md"
- name: Generate updater manifest
env:
VERSION: ${{ needs.validate.outputs.version }}
TAG: ${{ needs.validate.outputs.tag }}
run: |
bun apps/examples/desktop-app/scripts/generate-update-manifest.ts \
--version "$VERSION" \
--tag "$TAG" \
--dir dist/desktop \
--out dist/desktop/latest.json \
--repo "$GITHUB_REPOSITORY" \
--notes-file "$RUNNER_TEMP/release-notes.md"
cat dist/desktop/latest.json
- name: Get Previous Desktop Tag
id: prev_tag
env:
CURRENT_TAG: ${{ needs.validate.outputs.tag }}
run: |
PREV_TAG=$(git describe --tags --abbrev=0 --match 'desktop-v*' "$CURRENT_TAG^" 2>/dev/null || echo "")
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.validate.outputs.tag }}
name: "Desktop v${{ needs.validate.outputs.version }}"
# The repo-wide "latest" release stays owned by CLI releases; the
# desktop auto-update feed is the rolling desktop-latest release.
make_latest: "false"
files: dist/desktop/*
body: |
${{ steps.changelog.outputs.content }}
${{ steps.prev_tag.outputs.prev_tag != '' && format('**Full Changelog**: https://github.com/{0}/compare/{1}...{2}', github.repository, steps.prev_tag.outputs.prev_tag, needs.validate.outputs.tag) || '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update auto-update feed (desktop-latest)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if ! gh release view desktop-latest >/dev/null 2>&1; then
gh release create desktop-latest \
--title "Cline Code desktop (auto-update feed)" \
--notes "Rolling release backing the desktop app auto-updater. The latest.json asset points at the newest desktop-vX.Y.Z release. Do not delete." \
--latest=false \
--target "$(git rev-parse HEAD)"
fi
gh release upload desktop-latest dist/desktop/latest.json --clobber
- name: Summary
env:
VERSION: ${{ needs.validate.outputs.version }}
TAG: ${{ needs.validate.outputs.tag }}
run: |
echo "Published Cline Code desktop v${VERSION}"
echo "Release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
echo "Auto-update feed refreshed: https://github.com/${GITHUB_REPOSITORY}/releases/download/desktop-latest/latest.json"
- 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 Code desktop v${{ needs.validate.outputs.version }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "Cline Code desktop v${{ needs.validate.outputs.version }}"
- type: "section"
text:
type: "mrkdwn"
text: ${{ toJSON(steps.changelog.outputs.content) }}
- type: "context"
elements:
- type: "mrkdwn"
text: "<https://github.com/${{ github.repository }}/releases/tag/${{ needs.validate.outputs.tag }}|Download DMG> — installed apps auto-update on next launch${{ steps.prev_tag.outputs.prev_tag != '' && format(' | Full Changelog: https://github.com/{0}/compare/{1}...{2}', github.repository, steps.prev_tag.outputs.prev_tag, needs.validate.outputs.tag) || '' }}"
+551
View File
@@ -0,0 +1,551 @@
name: ext-vscode-ab-package
# Build (and optionally publish) the combined A/B VSIX: a tiny loader plus two
# complete extension bundles — `next/` from the SDK-based apps/vscode on main,
# `legacy/` from the legacy-extension branch. Cohort selection happens at
# runtime via PostHog flags; see apps/vscode-rollout/README.md for the design
# and the rollout runbook.
#
# Job layout: cheap input gates (preflight) and the two bundle test suites run
# ungated; the build job packages the VSIX with no environment attached, so
# publish=false rehearsals complete without any approval; only the publish job
# — Marketplace + Open VSX + bookkeeping — waits on the `publish` environment.
on:
workflow_dispatch:
inputs:
version:
description: "Combined VSIX version — must exceed every previously published version (e.g. 4.1.0)"
required: true
type: string
next-ref:
description: "Ref to build the next (SDK) bundle from"
required: true
default: "main"
type: string
legacy-ref:
description: "Ref to build the legacy bundle from"
required: true
default: "legacy-extension"
type: string
publish:
description: "Publish to the VS Code Marketplace and Open VSX (unchecked: just build the .vsix artifact)"
required: true
default: false
type: boolean
permissions:
contents: read
concurrency:
group: ext-vscode-ab-package-${{ github.event.inputs.version }}
cancel-in-progress: false
jobs:
# Input gates that need no checkout: fail in seconds — before the test
# suites, the ~20-minute build, and the environment approval — instead of
# at publish time.
preflight:
name: Validate inputs
runs-on: ubuntu-latest
steps:
# The input reaches the shell ONLY via env here (never inline
# expression interpolation, which is evaluated before bash runs and
# would allow script injection from the dispatch form). Because
# every later job `needs` preflight, passing this regex is what
# makes the plain-string `${{ inputs.version }}` interpolations
# downstream safe.
- name: Validate version format
env:
VERSION: ${{ github.event.inputs.version }}
run: |
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: version must be plain X.Y.Z with no leading 'v' and no suffix (got '$VERSION')."
echo "It is stamped verbatim into the union manifest and both bundle manifests."
exit 1
fi
echo "Version format ok: $VERSION"
# The reusable bun suite tests the dispatch revision (main), so
# publishing any other next-ref would ship an untested bundle.
# Build-only runs (publish=false) may still use arbitrary next-refs
# for artifact rehearsals.
- name: Refuse to publish an untested next-ref
if: ${{ github.event.inputs.publish == 'true' && github.event.inputs.next-ref != 'main' }}
run: |
echo "Error: publish=true requires next-ref=main — the test gate only covers main."
exit 1
# Marketplace versions are monotonic and cannot be unpublished:
# every publish must exceed the highest version ever published to
# the claude-dev listing FROM ANY BRANCH (combined stable or legacy
# hotfix). The publish job re-checks right before publishing — the
# environment-approval wait can last days and a legacy hotfix can
# land in between. Keep both copies of this check in sync.
- name: Verify version exceeds the live Marketplace version
if: ${{ github.event.inputs.publish == 'true' }}
env:
VERSION: ${{ github.event.inputs.version }}
run: |
LIVE=$(curl -sf --retry 3 -X POST "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery" \
-H "Content-Type: application/json" -H "Accept: application/json;api-version=3.0-preview.1" \
--data '{"filters":[{"criteria":[{"filterType":7,"value":"saoudrizwan.claude-dev"}]}],"flags":16}' \
| node -e 'let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>{process.stdout.write(JSON.parse(d).results[0].extensions[0].versions[0].version)})')
if [[ -z "$LIVE" ]]; then
echo "Error: could not resolve the live Marketplace version for saoudrizwan.claude-dev."
exit 1
fi
node -e '
const [next, live] = process.argv.slice(1).map((v) => v.split(".").map(Number));
for (let i = 0; i < 3; i++) {
if (next[i] > live[i]) process.exit(0);
if (next[i] < live[i]) break;
}
console.error(`Error: version ${process.argv[1]} does not exceed the live Marketplace version ${process.argv[2]}.`);
process.exit(1);
' "$VERSION" "$LIVE"
echo "Version ok: $VERSION exceeds live Marketplace version $LIVE"
# Gate the build/publish on BOTH bundles' own test suites, mirroring the two
# standalone publish paths (nightly gates on the bun suite via the same
# reusable workflow; the legacy publish inlines the npm suite).
#
# Caveat (shared with the nightly workflow): the reusable bun suite tests the
# DISPATCH revision — main's tip at dispatch, since this workflow is only
# dispatched from main — not `next-ref`. The build job therefore pins the
# default next-ref checkout to that same revision (tested == built) and
# preflight refuses publish=true for any other next-ref; build-only artifact
# runs may still build untested refs.
test-next:
name: Test next (SDK) bundle
permissions:
contents: read
pull-requests: read
uses: ./.github/workflows/ext-vscode-test.yml
# The legacy branch is the npm codebase, so the bun-based reusable workflow
# cannot test it. Inlined npm steps, kept in sync with the `test` job in
# ext-vscode-publish-legacy.yml (same suite, different ref input name).
test-legacy:
name: Test legacy bundle
runs-on: ubuntu-latest
# The tested revision, exported so the build job builds EXACTLY what
# this suite ran against. legacy-ref is a mutable branch name and the
# build job starts later — re-resolving the name there could pick up
# commits this gate never saw.
outputs:
tested-sha: ${{ steps.rev.outputs.sha }}
defaults:
run:
working-directory: apps/vscode
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.legacy-ref }}
- name: Record tested revision
id: rev
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci
- name: Run Quality Checks (lint + typecheck)
run: npm run ci:check-all
- name: Build Tests and Extension
id: build_step
run: npm run ci:build
- name: Unit Tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: npm run test:unit
- name: Extension Integration Tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: xvfb-run -a npm run test:coverage
- name: Webview Tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: |
cd webview-ui
npm run test:coverage
build:
name: Build combined (legacy + next) VSIX
needs: [preflight, test-next, test-legacy]
runs-on: ubuntu-latest
steps:
# For the default next-ref (main), pin the checkout to the exact
# revision the test-next gate ran against: a moving branch name could
# otherwise drift past the tested commit during the test phase.
- name: Checkout next (SDK) source
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.next-ref == 'main' && github.sha || github.event.inputs.next-ref }}
path: next-src
lfs: true
# Fail fast (before the ~20-min build) if a real publish is missing
# its changelog entry — same contract the standalone publish
# workflows enforce. Build-only rehearsals are exempt.
- name: Verify changelog entry
if: ${{ github.event.inputs.publish == 'true' }}
working-directory: next-src
run: |
EXPECTED_HEADING="## [${{ github.event.inputs.version }}]"
FIRST_HEADING=$(grep -m 1 '^## \[' CHANGELOG.md || true)
if [[ "$FIRST_HEADING" != "$EXPECTED_HEADING" ]]; then
echo "Error: CHANGELOG.md must start with '$EXPECTED_HEADING' before publishing (found '$FIRST_HEADING')."
exit 1
fi
echo "Found changelog entry for ${{ github.event.inputs.version }}"
# Pin to the revision test-legacy actually tested (see that job's
# outputs comment) — never re-resolve the mutable branch name here.
- name: Checkout legacy source
uses: actions/checkout@v4
with:
ref: ${{ needs.test-legacy.outputs.tested-sha }}
path: legacy-src
lfs: true
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
# --frozen-lockfile so the built bundle resolves the exact
# dependency set the test-next gate ran against (the reusable suite
# installs frozen too) — a bare install could silently re-resolve.
- name: Install next workspace dependencies
working-directory: next-src
run: bun install --frozen-lockfile
# @cline/* are local workspace symlinks to source packages; apps/vscode's
# `package` script does NOT build them, so without this the esbuild step
# fails on a fresh checkout. (The nightly workflow already does this.)
- name: Build SDK packages
working-directory: next-src
run: bun run build:sdk
- name: Assert better-sqlite3 native binary present
working-directory: next-src/apps/vscode
run: |
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
if [ ! -f "$NODE_FILE" ]; then
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
echo "(bun trustedDependencies postinstall likely did not run)"
exit 1
fi
echo "Found better-sqlite3 native binary: $NODE_FILE"
# Stamp the combined version into each bundle's package.json AFTER
# install and BEFORE its build: the About tab and telemetry
# extension_version read the bundle's own manifest, so without this
# the VSIX reports three different versions depending on where you
# look. (The nightly workflow gets the same alignment via nightlify.mjs.)
- name: Align next bundle version
working-directory: next-src/apps/vscode-rollout
run: node scripts/set-version.mjs --dir "$GITHUB_WORKSPACE/next-src/apps/vscode" --version "${{ github.event.inputs.version }}"
- name: Build next bundle
working-directory: next-src/apps/vscode
env:
CLINE_ENVIRONMENT: production
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
# Inlined by esbuild: attributes every telemetry event with
# extension_variant and unlocks the bundle's authoritative
# extension.rollout.bundle_activated capture. Rollout builds only.
CLINE_ROLLOUT_VARIANT: next
# Match the stable publish workflow's OpenTelemetry production defaults.
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: bun run package
- name: Install legacy dependencies
working-directory: legacy-src
run: |
npm --prefix apps/vscode install --include=optional
npm --prefix apps/vscode/webview-ui install --include=optional
- name: Align legacy bundle version
working-directory: next-src/apps/vscode-rollout
run: node scripts/set-version.mjs --dir "$GITHUB_WORKSPACE/legacy-src/apps/vscode" --version "${{ github.event.inputs.version }}"
- name: Build legacy bundle
working-directory: legacy-src/apps/vscode
env:
CLINE_ENVIRONMENT: production
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ROLLOUT_VARIANT: legacy
# Match the stable publish workflow's OpenTelemetry production defaults.
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 package
- name: Build loader and run rollout tests
working-directory: next-src/apps/vscode-rollout
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
run: |
bun run typecheck
bun run test
bun run build:production
- name: Stitch combined VSIX staging
working-directory: next-src/apps/vscode-rollout
run: |
node scripts/stitch.mjs \
--next "$GITHUB_WORKSPACE/next-src/apps/vscode" \
--legacy "$GITHUB_WORKSPACE/legacy-src/apps/vscode" \
--loader dist/extension.js \
--version "${{ github.event.inputs.version }}" \
--out "$GITHUB_WORKSPACE/staging"
- name: Smoke-test loader against staging
working-directory: next-src/apps/vscode-rollout
run: node scripts/smoke-loader.mjs "$GITHUB_WORKSPACE/staging"
# This workflow publishes the STABLE identity. If nightlify ever leaks
# into this path the union manifest would ship under the wrong name.
# The bundle sub-manifest checks guard the set-version.mjs stamping:
# the About tab and telemetry extension_version read those files.
- name: Assert stable manifest identity
working-directory: staging
env:
EXPECTED_VERSION: ${{ github.event.inputs.version }}
run: |
node -e '
const assert = require("node:assert");
const expected = process.env.EXPECTED_VERSION;
const pkg = require("./package.json");
assert.equal(pkg.name, "claude-dev", `unexpected name ${pkg.name}`);
assert.equal(pkg.publisher, "saoudrizwan", `unexpected publisher ${pkg.publisher}`);
assert.equal(pkg.version, expected, `unexpected union version ${pkg.version}`);
for (const bundle of ["next", "legacy"]) {
const sub = require(`./${bundle}/package.json`);
assert.equal(sub.version, expected, `unexpected ${bundle} bundle version ${sub.version}`);
}
console.log(`stable identity ok: ${pkg.publisher}.${pkg.name}@${pkg.version} (bundle versions aligned)`);
'
- name: Package VSIX
working-directory: staging
run: |
npm install -g @vscode/vsce
# Preserve the narrowly scoped VSCE `sendgrid` scanner exemption used by
# both standalone bundle workflows. No SendGrid credential is intentionally
# supplied here; inspect the reported artifact before widening the exemption.
vsce package --no-dependencies --allow-package-secrets sendgrid --out "claude-dev-${{ github.event.inputs.version }}.vsix"
- name: Upload VSIX artifact
uses: actions/upload-artifact@v4
with:
name: claude-dev-${{ github.event.inputs.version }}
path: staging/claude-dev-${{ github.event.inputs.version }}.vsix
if-no-files-found: error
publish:
name: Publish to Marketplace and Open VSX
needs: build
if: ${{ github.event.inputs.publish == 'true' }}
runs-on: ubuntu-latest
environment: publish
# contents: write is required by the post-publish bookkeeping (tag +
# GitHub Release), mirroring the standalone publish workflows.
permissions:
contents: write
steps:
# The built next revision: preflight refused publish=true for any
# next-ref other than main, and the build job pinned main to the
# dispatch SHA — so github.sha IS the published commit. Used for the
# changelog, the release tag, and the previous-tag lookup.
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Download VSIX artifact
uses: actions/download-artifact@v4
with:
name: claude-dev-${{ github.event.inputs.version }}
path: staging
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
# Re-check monotonicity at the last moment: the environment-approval
# wait can last days, and a legacy hotfix published in the meantime
# would otherwise be silently superseded by this older code line.
# Keep in sync with the preflight copy of this check.
- name: Re-verify version exceeds the live Marketplace version
env:
VERSION: ${{ github.event.inputs.version }}
run: |
LIVE=$(curl -sf --retry 3 -X POST "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery" \
-H "Content-Type: application/json" -H "Accept: application/json;api-version=3.0-preview.1" \
--data '{"filters":[{"criteria":[{"filterType":7,"value":"saoudrizwan.claude-dev"}]}],"flags":16}' \
| node -e 'let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>{process.stdout.write(JSON.parse(d).results[0].extensions[0].versions[0].version)})')
if [[ -z "$LIVE" ]]; then
echo "Error: could not resolve the live Marketplace version for saoudrizwan.claude-dev."
exit 1
fi
node -e '
const [next, live] = process.argv.slice(1).map((v) => v.split(".").map(Number));
for (let i = 0; i < 3; i++) {
if (next[i] > live[i]) process.exit(0);
if (next[i] < live[i]) break;
}
console.error(`Error: version ${process.argv[1]} does not exceed the live Marketplace version ${process.argv[2]}.`);
process.exit(1);
' "$VERSION" "$LIVE"
echo "Version ok: $VERSION exceeds live Marketplace version $LIVE"
# Both PATs are verified BEFORE the first irreversible publish so a
# missing Open VSX token can't strand us half-published. The two
# registries are separate steps: if Open VSX fails after the
# Marketplace accepted the VSIX, the run goes red (so the operator
# notices Open VSX lagged) but the bookkeeping below still runs —
# it is keyed off the Marketplace outcome, which is what "shipped"
# means for this listing.
- name: Publish to Marketplace
id: publish_marketplace
working-directory: staging
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: |
if [[ -z "$VSCE_PAT" ]]; then
echo "Error: VSCE_PAT is required to publish."
exit 1
fi
if [[ -z "$OVSX_PAT" ]]; then
echo "Error: OVSX_PAT is required to publish to Open VSX."
exit 1
fi
vsce publish --no-dependencies --packagePath "claude-dev-${{ github.event.inputs.version }}.vsix"
- name: Publish to Open VSX
working-directory: staging
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: npx ovsx publish --packagePath "claude-dev-${{ github.event.inputs.version }}.vsix" --pat "$OVSX_PAT"
# ---- Post-publish bookkeeping (tag / GitHub Release / Slack) ----
# Mirrors the standalone publish workflows. Every step here is
# continue-on-error, and gated on the MARKETPLACE outcome rather
# than plain step ordering: the Marketplace publish already
# happened, so bookkeeping must still run when only the Open VSX
# step failed, and a red run after a successful publish is exactly
# the confusion the nightly workflow taught us to avoid (tag pushes
# fail whenever the built commit touches .github/workflows/** — no
# grantable permission fixes that; push the tag manually in that
# case, see the publish-extension skill).
- name: Extract changelog entry
id: changelog
if: ${{ !cancelled() && steps.publish_marketplace.outcome == 'success' }}
continue-on-error: true
run: |
CONTENT=$(awk '/^## \[/{if(found) exit; found=1; next} found{print}' CHANGELOG.md)
{
echo "content<<CHANGELOG_EOF"
echo "$CONTENT"
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
- name: Resolve previous release tag
id: prev_tag
if: ${{ !cancelled() && steps.publish_marketplace.outcome == 'success' }}
continue-on-error: true
run: |
# ls-remote needs no local tag objects; take the highest v* tag
# below the one being released.
PREV=$(git ls-remote --tags origin 'v*' \
| awk -F/ '{print $NF}' | grep -v '\^{}' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| grep -vx "v${{ github.event.inputs.version }}" \
| sort -V | tail -1)
echo "prev_tag=$PREV" >> "$GITHUB_OUTPUT"
- name: Create and push release tag
if: ${{ !cancelled() && steps.publish_marketplace.outcome == 'success' }}
continue-on-error: true
run: |
TAG="v${{ github.event.inputs.version }}"
git tag "$TAG" HEAD
git push origin "refs/tags/$TAG"
echo "Pushed $TAG at $(git rev-parse HEAD)"
- name: Create GitHub Release
if: ${{ !cancelled() && steps.publish_marketplace.outcome == 'success' }}
continue-on-error: true
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ github.event.inputs.version }}
files: staging/claude-dev-${{ github.event.inputs.version }}.vsix
body: |
${{ steps.changelog.outputs.content }}
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...v${{ github.event.inputs.version }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Post release to Slack
if: ${{ !cancelled() && steps.publish_marketplace.outcome == 'success' }}
continue-on-error: true
uses: slackapi/slack-github-action@v3.0.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
payload: |
channel: "C0APVKGGZFC"
text: "Cline v${{ github.event.inputs.version }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Cline v${{ github.event.inputs.version }}*"
- 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 }}...v${{ github.event.inputs.version }}"
@@ -0,0 +1,294 @@
name: ext-vscode-publish-legacy
# Publishes the legacy (pre-SDK-migration) VS Code extension from the
# `legacy-extension` branch. This branch holds the npm-based 3.89.x codebase,
# rolled forward under a 4.0.x version so existing 4.0.0 users still receive
# the update. The main `ext-vscode-publish-stable.yml` workflow (bun-based)
# stays the path for releasing main once the SDK migration is solid.
#
# This workflow lives on and is dispatched from `main` (so it satisfies the
# default-branch dispatch requirement), but it checks out and builds the
# `legacy-extension` branch.
on:
workflow_dispatch:
inputs:
release-type:
description: "Choose release type (release or pre-release)"
required: true
default: "release"
type: choice
options:
- pre-release
- release
branch:
description: "Branch holding the legacy extension code"
required: true
default: "legacy-extension"
type: string
permissions:
contents: write
packages: write
checks: write
pull-requests: write
concurrency:
group: ext-vscode-publish-legacy-${{ github.event.inputs.branch }}
cancel-in-progress: false
jobs:
# Gate the publish on the legacy branch's own npm-based test suite. We can't
# reuse ./.github/workflows/ext-vscode-test.yml here — on main that's the
# bun-based suite and it would test main, not the legacy branch — so the
# essential quality + test steps are inlined against the checked-out legacy
# branch.
test:
name: Test Legacy Extension
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/vscode
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci
- name: Run Quality Checks (lint + typecheck)
run: npm run ci:check-all
- name: Build Tests and Extension
id: build_step
run: npm run ci:build
- name: Unit Tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: npm run test:unit
- name: Extension Integration Tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: xvfb-run -a npm run test:coverage
- name: Webview Tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: |
cd webview-ui
npm run test:coverage
publish:
needs: test
name: Publish Legacy Extension
runs-on: ubuntu-latest
environment: publish
defaults:
run:
working-directory: apps/vscode
steps:
# Check out the legacy branch (NOT main). fetch-depth: 0 + tags so we
# can create/push the release tag and compute the previous tag.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
fetch-tags: true
lfs: true
- name: Resolve Release Tag
id: resolve_tag
working-directory: ${{ github.workspace }}
env:
BRANCH: ${{ github.event.inputs.branch }}
run: |
# Tag is derived from the package version on the legacy branch.
VERSION=$(node -p "require('./apps/vscode/package.json').version")
TAG="v$VERSION"
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then
echo "Error: derived tag '$TAG' does not match vX.Y.Z"
exit 1
fi
TAG_REF="refs/tags/$TAG"
HEAD_SHA=$(git rev-parse HEAD)
if git show-ref --verify --quiet "$TAG_REF"; then
TAG_SHA=$(git rev-list -n 1 "$TAG_REF^{commit}")
if [[ "$TAG_SHA" != "$HEAD_SHA" ]]; then
echo "Error: tag '$TAG' already exists at $TAG_SHA, not at branch head ($HEAD_SHA)"
exit 1
fi
echo "Tag '$TAG' already exists at branch head. Continuing."
else
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "$TAG" "$HEAD_SHA"
git push origin "$TAG_REF"
echo "Created and pushed tag '$TAG' from $BRANCH head $HEAD_SHA."
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode install --include=optional
- name: Install webview-ui dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui install --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
- name: Get Version
id: get_version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Verify Tag Matches Package Version
run: |
TAG="${{ steps.resolve_tag.outputs.tag }}"
VERSION="v${{ steps.get_version.outputs.version }}"
if [[ "$TAG" != "$VERSION" ]]; then
echo "Error: tag '$TAG' does not match package version '$VERSION'"
exit 1
fi
echo "Tag and package version match: $TAG"
- name: Verify Changelog Entry
working-directory: ${{ github.workspace }}
run: |
EXPECTED_HEADING="## [${{ steps.get_version.outputs.version }}]"
FIRST_HEADING=$(grep -m 1 '^## \[' CHANGELOG.md || true)
if [[ "$FIRST_HEADING" != "$EXPECTED_HEADING" ]]; then
echo "Error: CHANGELOG.md must start with '$EXPECTED_HEADING' before publishing."
echo "Current first release heading: ${FIRST_HEADING:-<none>}"
exit 1
fi
echo "Found changelog entry for ${{ steps.get_version.outputs.version }}"
- name: Verify Marketplace Tokens
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: |
if [[ -z "$VSCE_PAT" ]]; then
echo "Error: VSCE_PAT is required to publish the stable VS Code extension."
exit 1
fi
if [[ -z "$OVSX_PAT" ]]; then
echo "Error: OVSX_PAT is required to publish the stable Open VSX extension."
exit 1
fi
echo "Marketplace publish tokens are configured."
- name: Package and Publish Extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
CLINE_ENVIRONMENT: production
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
RELEASE_TYPE: ${{ github.event.inputs.release-type }}
run: |
# Swap README.marketplace.md into README.md so both the GitHub
# release artifact (vsce package below) and the marketplace
# publish (npm run publish:marketplace below, which swaps
# internally as an idempotent no-op) ship the same README.
node scripts/marketplace-readme.mjs swap-in
trap 'node scripts/marketplace-readme.mjs restore' EXIT
# Required to generate the .vsix
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
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
npm run publish:marketplace
echo "Successfully published release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
fi
- name: Get Previous Tag
id: prev_tag
working-directory: ${{ github.workspace }}
run: |
CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}"
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null || echo "")
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
- name: Get Changelog Entry
id: changelog
working-directory: ${{ github.workspace }}
run: |
# Get content between first ## [ and second ## [
CONTENT=$(awk '/^## \[/{if(found) exit; found=1; next} found{print}' CHANGELOG.md)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.resolve_tag.outputs.tag }}
files: "apps/vscode/*.vsix"
body: |
${{ steps.changelog.outputs.content }}
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.resolve_tag.outputs.tag }}
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 }} (legacy)"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Cline ${{ steps.resolve_tag.outputs.tag }} (legacy)*"
- 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 }}"
+233 -35
View File
@@ -1,14 +1,40 @@
name: ext-vscode-publish-nightly
# Publishes saoudrizwan.cline-nightly as the COMBINED A/B VSIX: the rollout
# loader plus two complete extension bundles — `next/` from this ref's
# apps/vscode (SDK-based) and `legacy/` from the legacy-extension branch.
# Cohort selection happens at runtime via PostHog flags; see
# apps/vscode-rollout/README.md for the design and rollout runbook.
#
# The stable-identity equivalent of this pipeline is ext-vscode-ab-package.yml
# (manual dispatch, publishes claude-dev). Shared logic lives in
# apps/vscode-rollout/scripts (nightlify/gen-manifest/stitch/smoke) so both
# workflows stay thin. The single-bundle nightly path this replaced
# (apps/vscode/scripts/publish-nightly.mjs) remains for manual feature-branch
# pre-release publishes.
on:
schedule:
# Every day at 4:00 AM PST (12:00 UTC)
- cron: "0 12 * * *"
workflow_dispatch:
inputs:
legacy-ref:
description: "Ref to build the legacy bundle from"
required: false
default: "legacy-extension"
type: string
dry-run:
description: "Build and upload the .vsix artifact without publishing or tagging"
required: false
default: false
type: boolean
run-name: "Publish Nightly from ${{ github.ref_name }} @ ${{ github.sha }}"
run-name: "Publish Combined Nightly from ${{ github.ref_name }} @ ${{ github.sha }}"
# Prevent concurrent publish runs on the same branch. The nightly publish script
# generates the extension version from a seconds-resolution timestamp, so parallel
# runs on the same ref can collide on the same version and cause publish failures
# or inconsistent tagging. Runs on different branches proceed independently.
# Prevent concurrent publish runs on the same branch: the version is generated
# from a seconds-resolution timestamp, so parallel runs on the same ref can
# collide on the same version and cause publish failures or inconsistent tagging.
concurrency:
group: ext-vscode-publish-nightly-${{ github.ref }}
cancel-in-progress: false
@@ -17,7 +43,7 @@ permissions: {}
jobs:
test:
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
if: github.repository == 'cline/cline'
permissions:
contents: read
pull-requests: read
@@ -27,54 +53,106 @@ jobs:
needs: test
permissions:
contents: write
name: Publish Cline (Nightly) Extension
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
name: Publish Cline (Nightly) Combined Extension
# Defense in depth: only protected main may enter the publishing environment.
# This `if` is advisory because a dispatched branch runs its own copy of this
# file; the enforced gate is the PublishNightly environment's deployment-branch
# policy, which must also allow only main.
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: PublishNightly
defaults:
run:
working-directory: apps/vscode
steps:
- name: Checkout selected branch
- name: Checkout next (SDK) source
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
path: next-src
lfs: true
persist-credentials: false
- name: Show build source
working-directory: ${{ github.workspace }}
run: |
echo "Building ref: $GITHUB_REF"
echo "Building sha: $GITHUB_SHA"
git --no-pager log -1 --oneline
- name: Checkout legacy source
uses: actions/checkout@v4
with:
# NOTE: inputs are empty strings on `schedule` events, so the ||
# fallback (not the input's declared default) is what the cron uses.
ref: ${{ inputs.legacy-ref || 'legacy-extension' }}
path: legacy-src
lfs: true
persist-credentials: false
- name: Show build sources
env:
# Routed through env rather than interpolated into the script body so
# a crafted dispatch input can't inject shell (hygiene: dispatchers
# need write access anyway, but keep the pattern clean).
LEGACY_REF: ${{ inputs.legacy-ref || 'legacy-extension' }}
run: |
echo "next: $(git -C next-src rev-parse HEAD)"
echo "legacy: $(git -C legacy-src rev-parse HEAD) ($LEGACY_REF)"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
# Node is required beyond install: the rollout scripts run under node and
# publishing shells out to vsce/ovsx. Pinned to Node 22 because newer LTS
# (Node 24 / npm 11) can make vsce's dependency detection fail.
- 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 extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci --include=optional
# ONE version for the next bundle, the legacy bundle, and the union
# manifest: gen-manifest hard-fails if the bundle identities diverge.
# Same scheme as the standalone nightly: <major>.<minor>.<unix-seconds>
# from next's base version, so it keeps outranking earlier nightlies.
- name: Compute nightly version
id: version
run: |
BASE=$(node -p "require('./next-src/apps/vscode/package.json').version")
VERSION="$(echo "$BASE" | cut -d. -f1,2).$(date +%s)"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Combined nightly version: $VERSION (base $BASE)"
- name: Install webview-ui dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci --include=optional
- name: Install next workspace dependencies
working-directory: next-src
run: bun install --frozen-lockfile
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
- name: Build SDK packages
working-directory: next-src
run: bun run build:sdk
- name: Publish Nightly Extension
- name: Assert better-sqlite3 native binary present
working-directory: next-src/apps/vscode
run: |
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
if [ ! -f "$NODE_FILE" ]; then
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
echo "(bun trustedDependencies postinstall likely did not run)"
exit 1
fi
echo "Found better-sqlite3 native binary: $NODE_FILE"
# Rewrite each bundle's package.json to the cline-nightly identity BEFORE
# its build (runtime command/config IDs derive from the manifest) and
# AFTER dependency install (workspace self-links key off the original
# package name).
- name: Nightlify next bundle manifest
working-directory: next-src/apps/vscode-rollout
run: node scripts/nightlify.mjs --dir "$GITHUB_WORKSPACE/next-src/apps/vscode" --version "${{ steps.version.outputs.version }}"
- name: Build next bundle
working-directory: next-src/apps/vscode
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
CLINE_ENVIRONMENT: production
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
# Inlined by esbuild: attributes every telemetry event with
# extension_variant and unlocks the bundle's authoritative
# extension.rollout.bundle_activated capture. Rollout builds only.
CLINE_ROLLOUT_VARIANT: next
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
@@ -82,10 +160,129 @@ 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 }}
run: npm run publish:marketplace:nightly
run: bun run package
- name: Install legacy dependencies
working-directory: legacy-src
run: |
npm --prefix apps/vscode install --include=optional
npm --prefix apps/vscode/webview-ui install --include=optional
- name: Nightlify legacy bundle manifest
working-directory: next-src/apps/vscode-rollout
run: node scripts/nightlify.mjs --dir "$GITHUB_WORKSPACE/legacy-src/apps/vscode" --version "${{ steps.version.outputs.version }}"
- name: Build legacy bundle
working-directory: legacy-src/apps/vscode
env:
CLINE_ENVIRONMENT: production
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ROLLOUT_VARIANT: legacy
# Legacy's esbuild inlines these too (its own publish workflow passes
# them) — omitting them here would ship the legacy bundle with the
# OTel pipeline dead, unlike what legacy users get today.
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 package
- name: Build loader and run rollout tests
working-directory: next-src/apps/vscode-rollout
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
run: |
bun run typecheck
bun run test
bun run build:production
- name: Stitch combined VSIX staging
working-directory: next-src/apps/vscode-rollout
run: |
node scripts/stitch.mjs \
--next "$GITHUB_WORKSPACE/next-src/apps/vscode" \
--legacy "$GITHUB_WORKSPACE/legacy-src/apps/vscode" \
--loader dist/extension.js \
--version "${{ steps.version.outputs.version }}" \
--out "$GITHUB_WORKSPACE/staging"
- name: Smoke-test loader against staging
working-directory: next-src/apps/vscode-rollout
run: node scripts/smoke-loader.mjs "$GITHUB_WORKSPACE/staging"
# The nightly identity must have fully propagated (nightlify -> both
# bundle manifests -> union manifest) or we'd publish over the stable
# extension ID. The bundle sub-manifest checks guard the version
# stamping: the About tab and telemetry extension_version read those.
- name: Assert nightly manifest identity
working-directory: staging
env:
EXPECTED_VERSION: ${{ steps.version.outputs.version }}
run: |
node -e '
const assert = require("node:assert");
const expected = process.env.EXPECTED_VERSION;
const pkg = require("./package.json");
assert.equal(pkg.name, "cline-nightly", `unexpected name ${pkg.name}`);
assert.equal(pkg.publisher, "saoudrizwan", `unexpected publisher ${pkg.publisher}`);
assert.equal(pkg.version, expected, `unexpected union version ${pkg.version}`);
for (const bundle of ["next", "legacy"]) {
const sub = require(`./${bundle}/package.json`);
assert.equal(sub.name, "cline-nightly", `unexpected ${bundle} bundle name ${sub.name}`);
assert.equal(sub.version, expected, `unexpected ${bundle} bundle version ${sub.version}`);
}
console.log(`nightly identity ok: ${pkg.publisher}.${pkg.name}@${pkg.version} (bundle identities aligned)`);
'
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
- name: Package VSIX
working-directory: staging
# Preserve the narrowly scoped VSCE `sendgrid` scanner exemption used by
# both standalone bundle workflows. No SendGrid credential is intentionally
# supplied here; inspect the reported artifact before widening the exemption.
run: vsce package --no-dependencies --allow-package-secrets sendgrid --out "cline-nightly-${{ steps.version.outputs.version }}.vsix"
- name: Upload VSIX artifact
uses: actions/upload-artifact@v4
with:
name: cline-nightly-${{ steps.version.outputs.version }}
path: staging/cline-nightly-${{ steps.version.outputs.version }}.vsix
if-no-files-found: error
# The job is main-only; step-level dry-run gating still permits a build-only
# rehearsal without publishing or tagging.
- name: Publish to VS Code Marketplace and Open VSX
if: github.ref == 'refs/heads/main' && inputs.dry-run != true
working-directory: staging
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: |
if [[ -z "$VSCE_PAT" ]]; then
echo "Error: VSCE_PAT is required to publish."
exit 1
fi
vsce publish --no-dependencies --packagePath "cline-nightly-${{ steps.version.outputs.version }}.vsix"
if [[ -n "$OVSX_PAT" ]]; then
npx ovsx publish --packagePath "cline-nightly-${{ steps.version.outputs.version }}.vsix" --pat "$OVSX_PAT"
else
echo "WARNING: OVSX_PAT not set; skipping Open VSX publish."
fi
- name: Tag published commit
working-directory: ${{ github.workspace }}
if: github.ref == 'refs/heads/main' && inputs.dry-run != true
# Best-effort bookkeeping: the default GITHUB_TOKEN cannot create a ref
# whose commit modifies workflow files (no workflows permission exists
# for it), so this step fails whenever HEAD touched .github/workflows.
# The publish already succeeded by this point — don't mark the run red;
# push the tag manually with user credentials when it matters.
continue-on-error: true
working-directory: next-src
env:
GH_TOKEN: ${{ github.token }}
run: |
@@ -93,10 +290,11 @@ jobs:
SHORT_SHA=$(git rev-parse --short=12 HEAD)
TIMESTAMP=$(date -u +"%Y%m%d%H%M%S")
TAG="nightly-${SAFE_REF}-${TIMESTAMP}-${SHORT_SHA}"
LEGACY_SHA=$(git -C "$GITHUB_WORKSPACE/legacy-src" rev-parse HEAD)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "Cline Nightly published from ${GITHUB_REF_NAME} at ${GITHUB_SHA}"
git tag -a "$TAG" -m "Cline Nightly (combined A/B) published from ${GITHUB_REF_NAME} at ${GITHUB_SHA} (legacy bundle: ${LEGACY_SHA})"
# Use an explicit HTTPS remote with GH_TOKEN because checkout was run with
# persist-credentials: false, so actions/checkout did not persist a git credential helper.
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "refs/tags/${TAG}"
+108 -27
View File
@@ -27,6 +27,10 @@ permissions:
checks: write
pull-requests: write
concurrency:
group: ext-vscode-publish-stable-${{ github.event.inputs.tag }}
cancel-in-progress: false
jobs:
test:
uses: ./.github/workflows/ext-vscode-test.yml
@@ -102,26 +106,61 @@ jobs:
echo "Error: tag '$TAG' does not exist in the repository"
exit 1
fi
echo "Using existing tag '$TAG'."
TAG_SHA=$(git rev-list -n 1 "$TAG_REF^{commit}")
if [[ "$TAG_SHA" != "$TESTED_SHA" ]]; then
echo "Error: existing tag '$TAG' points to $TAG_SHA, but this workflow tested $TESTED_SHA"
echo "Dispatch from the tag ref, or from the exact main commit the tag points to."
exit 1
fi
echo "Using existing tag '$TAG' at tested SHA $TESTED_SHA."
fi
git checkout --detach "$TAG_REF^{commit}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "resolved_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
# Node is still REQUIRED in the publish job (not just for install): the
# publish scripts run as `node scripts/publish-*.mjs`, the version step uses
# `node -p`, and `npx ovsx` needs npm. setup-bun does not provide a Node
# runtime, so keep setup-node. Pinned to Node 22 because newer LTS
# (Node 24 / npm 11) can make vsce's `npm list` detection fail with
# ELSPROBLEMS during packaging.
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install extension dependencies
# Single root install resolves the whole bun workspace at once (replaces the
# per-package `npm install` steps for apps/vscode + webview-ui).
- name: Install workspace dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode install --include=optional
run: bun install --frozen-lockfile
- name: Install webview-ui dependencies
# @cline/* are local workspace symlinks to source packages; build dist/ before
# packaging/publishing the extension.
- name: Build SDK packages
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui install --include=optional
run: bun run build:sdk
- name: Assert better-sqlite3 native binary present
run: |
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
if [ ! -f "$NODE_FILE" ]; then
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
echo "(bun trustedDependencies postinstall likely did not run)"
exit 1
fi
echo "Found better-sqlite3 native binary: $NODE_FILE"
# vsce is a workspace devDependency (on node_modules/.bin), but ovsx is not
# vendored and the publish script invokes it via `npx ovsx`, so install ovsx
# globally (npm is available via setup-node). vsce is installed globally too
# to preserve the script's existing PATH expectations.
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
@@ -141,6 +180,60 @@ jobs:
fi
echo "Tag and package version match: $TAG"
- name: Verify Changelog Entry
working-directory: ${{ github.workspace }}
run: |
EXPECTED_HEADING="## [${{ steps.get_version.outputs.version }}]"
FIRST_HEADING=$(grep -m 1 '^## \[' CHANGELOG.md || true)
if [[ "$FIRST_HEADING" != "$EXPECTED_HEADING" ]]; then
echo "Error: CHANGELOG.md must start with '$EXPECTED_HEADING' before publishing."
echo "Current first release heading: ${FIRST_HEADING:-<none>}"
exit 1
fi
echo "Found changelog entry for ${{ steps.get_version.outputs.version }}"
- name: Verify Marketplace Tokens
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: |
if [[ -z "$VSCE_PAT" ]]; then
echo "Error: VSCE_PAT is required to publish the stable VS Code extension."
exit 1
fi
if [[ -z "$OVSX_PAT" ]]; then
echo "Error: OVSX_PAT is required to publish the stable Open VSX extension."
exit 1
fi
echo "Marketplace publish tokens are configured."
- name: Get Previous Tag
id: prev_tag
working-directory: ${{ github.workspace }}
run: |
CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}"
PREV_TAG=$(
git tag --merged "$CURRENT_TAG^" --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$' \
| head -n 1 || true
)
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
- name: Get Changelog Entry
id: changelog
working-directory: ${{ github.workspace }}
run: |
# Get content between the matching version heading and the next release heading.
CONTENT=$(awk -v version="${{ steps.get_version.outputs.version }}" '
$0 == "## [" version "]" { found=1; next }
found && /^## \[/ { exit }
found { print }
END { if (!found) exit 1 }
' CHANGELOG.md)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Package and Publish Extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
@@ -164,35 +257,23 @@ jobs:
node scripts/marketplace-readme.mjs swap-in
trap 'node scripts/marketplace-readme.mjs restore' EXIT
# Required to generate the .vsix
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
# Required to generate the .vsix. --no-dependencies: the extension
# is fully esbuild-bundled, and under the bun workspace the @cline/*
# deps are symlinks pointing outside the package, so without this vsce
# would walk them and pull the whole monorepo into the .vsix.
vsce package --no-dependencies --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
# These scripts run under `node scripts/publish-marketplace.mjs`;
# bun run just launches them. Node + npm (for `npx ovsx`) come from
# setup-node above.
if [ "$RELEASE_TYPE" = "pre-release" ]; then
npm run publish:marketplace:prerelease
bun run publish:marketplace:prerelease
echo "Successfully published pre-release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
else
npm run publish:marketplace
bun run publish:marketplace
echo "Successfully published release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
fi
- name: Get Previous Tag
id: prev_tag
working-directory: ${{ github.workspace }}
run: |
CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}"
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null || echo "")
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
- name: Get Changelog Entry
id: changelog
working-directory: ${{ github.workspace }}
run: |
# Get content between first ## [ and second ## [
CONTENT=$(awk '/^## \[/{if(found) exit; found=1; next} found{print}' CHANGELOG.md)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
+44 -27
View File
@@ -45,12 +45,16 @@ jobs:
- 'apps/vscode/assets/**'
- 'apps/vscode/walkthrough/**'
- 'apps/vscode/package.json'
- 'apps/vscode/package-lock.json'
- 'apps/vscode/webview-ui/package.json'
# Root bun lockfile is authoritative for the whole workspace (incl. apps/vscode).
- 'bun.lock'
# SDK source packages are local workspace symlinks (@cline/*), so SDK changes affect the build.
- 'sdk/packages/**'
- 'apps/vscode/buf.yaml'
- 'apps/vscode/tsconfig*.json'
- 'apps/vscode/biome.jsonc'
- 'apps/vscode/esbuild.mjs'
- 'apps/vscode/.mocharc.json'
- 'apps/vscode/bunfig.toml'
- 'apps/vscode/.vscode-test.mjs'
- 'apps/vscode/.vscodeignore'
- 'apps/vscode/playwright*.ts'
@@ -84,26 +88,20 @@ jobs:
working-directory: apps/vscode
steps:
- uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: 22
bun-version: 1.3.14
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
# Cache Bun's global install cache - keyed on the authoritative root bun.lock.
- name: Cache Bun install cache
uses: actions/cache@v4
id: root-cache
id: bun-cache
with:
path: apps/vscode/node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('apps/vscode/package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: apps/vscode/webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('apps/vscode/webview-ui/package-lock.json') }}
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
# Cache VS Code installation
- name: Cache VS Code
@@ -124,22 +122,41 @@ jobs:
~/.cache/ms-playwright
~/Library/Caches/ms-playwright
~/AppData/Local/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('apps/vscode/package-lock.json') }}
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install extension dependencies
# Single root install resolves the entire bun workspace at once (replaces
# the per-package `npm ci` steps for apps/vscode + webview-ui).
- name: Install workspace dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci
run: bun install --frozen-lockfile
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
# @cline/* are local workspace symlinks to source packages; build dist/
# before building/packaging the extension for E2E.
- name: Build SDK packages
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci
run: bun run build:sdk
- name: Install vsce
run: npm install -g @vscode/vsce
- name: Assert better-sqlite3 native binary present
# Force bash: the Windows runner defaults to pwsh, which can't parse this
# POSIX test. Git Bash ships on GitHub's windows-latest images.
shell: bash
run: |
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
if [ ! -f "$NODE_FILE" ]; then
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
echo "(bun trustedDependencies postinstall likely did not run)"
exit 1
fi
echo "Found better-sqlite3 native binary: $NODE_FILE"
# NOTE: vsce is no longer installed globally. @vscode/vsce is a workspace
# devDependency of apps/vscode (resolved into node_modules/.bin), and the
# `test:e2e:build` script invokes `vsce` via `bun run`, which puts the local
# .bin on PATH. No global install needed.
- name: Install xvfb on Linux
if: matrix.runner == 'ubuntu'
@@ -148,11 +165,11 @@ jobs:
# Run optimized E2E tests (eliminates redundant builds)
- name: Run E2E tests - Linux
if: matrix.runner == 'ubuntu'
run: xvfb-run -a npm run test:e2e:optimal
run: xvfb-run -a bun run test:e2e:optimal
- name: Run E2E tests - Non-Linux
if: matrix.runner != 'ubuntu'
run: npm run test:e2e:optimal
run: bun run test:e2e:optimal
- uses: actions/upload-artifact@v4
if: ${{ failure() }}
+123 -60
View File
@@ -45,13 +45,16 @@ jobs:
- 'apps/vscode/assets/**'
- 'apps/vscode/walkthrough/**'
- 'apps/vscode/package.json'
- 'apps/vscode/package-lock.json'
- 'apps/vscode/webview-ui/package.json'
# Root bun lockfile is authoritative for the whole workspace (incl. apps/vscode).
- 'bun.lock'
# SDK source packages are local workspace symlinks (@cline/*), so SDK changes affect the build.
- 'sdk/packages/**'
- 'apps/vscode/buf.yaml'
- 'apps/vscode/tsconfig*.json'
- 'apps/vscode/biome.jsonc'
- 'apps/vscode/esbuild.mjs'
- 'apps/vscode/.mocharc.json'
- 'apps/vscode/.nycrc*.json'
- 'apps/vscode/bunfig.toml'
- 'apps/vscode/.vscode-test.mjs'
- 'apps/vscode/test-setup.js'
- '.github/workflows/ext-vscode-test.yml'
@@ -60,9 +63,13 @@ jobs:
- 'apps/vscode/proto/**'
- 'apps/vscode/standalone/**'
- 'apps/vscode/testing-platform/**'
- 'apps/vscode/testing-platform/package.json'
- 'apps/vscode/tests/specs/**'
- 'apps/vscode/package.json'
- 'apps/vscode/package-lock.json'
# Root bun lockfile is authoritative for the whole workspace (incl. apps/vscode).
- 'bun.lock'
# SDK source packages are local workspace symlinks (@cline/*), so SDK changes affect the build.
- 'sdk/packages/**'
- 'apps/vscode/buf.yaml'
- 'apps/vscode/tsconfig*.json'
- 'apps/vscode/esbuild.mjs'
@@ -82,33 +89,44 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
bun-version: 1.3.14
- name: Install extension dependencies
# Single root install resolves the entire bun workspace (apps/vscode,
# webview-ui, testing-platform and the @cline/* SDK symlinks) at once,
# so the previous per-package `npm ci` steps collapse into one.
- name: Install workspace dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci
run: bun install --frozen-lockfile
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
# @cline/* are local workspace symlinks to source packages; their dist/
# output must be built before the extension can type-check/compile.
- name: Build SDK packages
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci
run: bun run build:sdk
- name: Assert better-sqlite3 native binary present
run: |
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
if [ ! -f "$NODE_FILE" ]; then
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
echo "(bun trustedDependencies postinstall likely did not run)"
exit 1
fi
echo "Found better-sqlite3 native binary: $NODE_FILE"
- name: Run Quality Checks (Parallel)
run: npm run ci:check-all
run: bun run ci:check-all
vscode-test:
needs: [detect-changes, quality-checks]
if: needs.detect-changes.outputs.vscode == 'true'
env:
VSCODE_TEST_VERSION: 1.103.0
VSCODE_TEST_VERSION: 1.101.0
strategy:
fail-fast: false
matrix:
@@ -123,30 +141,43 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
bun-version: 1.3.14
- name: Install extension dependencies
# Single root install resolves the entire bun workspace at once (replaces
# the per-package `npm ci` steps for apps/vscode + webview-ui).
- name: Install workspace dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci
run: bun install --frozen-lockfile
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
# @cline/* are local workspace symlinks to source packages; build dist/
# before compiling/testing the extension.
- name: Build SDK packages
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci
run: bun run build:sdk
- name: Set up NPM on Windows
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}
- name: Assert better-sqlite3 native binary present
run: |
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
if [ ! -f "$NODE_FILE" ]; then
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
echo "(bun trustedDependencies postinstall likely did not run)"
exit 1
fi
echo "Found better-sqlite3 native binary: $NODE_FILE"
# NOTE: The old `npm config set script-shell bash` step is intentionally
# removed. Scripts are now launched with `bun run`, which uses Bun's own
# built-in cross-platform shell rather than npm's configured script-shell,
# so that npm-specific Windows workaround no longer applies. Bash-dependent
# scripts (e.g. scripts/proto-lint.sh, standalone/runclinecore.sh) are
# invoked explicitly via `bash ...` from within the package scripts, and
# this job's `defaults.run.shell: bash` (Git Bash on Windows) still covers
# the workflow `run:` blocks below.
- name: Cache VS Code test runtime
if: runner.os == 'Windows'
@@ -158,24 +189,51 @@ jobs:
# Build the extension and tests (without redundant checks)
- name: Build Tests and Extension
id: build_step
run: npm run ci:build
run: bun run ci:build
- name: Unit Tests with coverage - Linux
- name: Vitest Suites (SDK adapter + model catalog)
id: vitest_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
# The vitest config sets passWithNoTests: true, so a broken glob/alias
# would "pass" with zero tests. Capture output and assert a non-zero
# test count to guard against silent skips.
run: |
set -o pipefail
bun run test:vitest 2>&1 | tee vitest-output.log
# Strip ANSI color codes before matching — vitest colorizes the
# "Tests N passed" summary, so the count is not adjacent to the
# "Tests" label in the raw bytes.
if ! sed -r 's/\x1b\[[0-9;]*m//g' vitest-output.log | grep -Eq 'Tests[[:space:]]+[0-9]*[1-9][0-9]* (passed|failed)'; then
echo "ERROR: vitest reported zero tests (possible silent skip)."
exit 1
fi
- name: Unit Tests (bun) - Linux
id: unit_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
# The node-side unit suite (.mocharc spec set) now runs under `bun test`
# via scripts/run-bun-unit-tests.ts (one isolated bun process per file).
# The runner exits non-zero on any failure and prints a final
# "Files: N Pass: P Fail: F" summary; assert a non-zero pass count to
# guard against an empty glob silently "passing".
run: |
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
set -o pipefail
bun run test:unit 2>&1 | tee unit-output.log
if ! grep -Eq 'Pass:[[:space:]]+[0-9]*[1-9][0-9]*' unit-output.log; then
echo "ERROR: bun unit runner reported zero passing tests (possible silent skip)."
exit 1
fi
- name: Unit Tests - Non-Linux
- name: Unit Tests (bun) - Non-Linux
id: unit_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: |
npm run test:unit
bun run test:unit
- name: Extension Integration Tests - Linux
id: integration_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
run: xvfb-run -a npm run test:coverage
run: xvfb-run -a bun run test:coverage
- name: Extension Integration Tests - Non-Linux
id: integration_tests_non_linux
@@ -183,7 +241,7 @@ jobs:
run: |
for attempt in 1 2 3; do
echo "Running extension integration tests (attempt ${attempt}/3)"
if npm run test:integration; then
if bun run test:integration; then
exit 0
fi
@@ -201,7 +259,7 @@ jobs:
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: |
cd webview-ui
npm run test:coverage
bun run test:coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
@@ -210,7 +268,6 @@ jobs:
with:
name: pr-coverage-reports
path: |
apps/vscode/coverage-unit/lcov.info
apps/vscode/webview-ui/coverage/lcov.info
test-platform-integration:
@@ -224,39 +281,45 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
apps/vscode/testing-platform/package-lock.json
bun-version: 1.3.14
- name: Install extension dependencies
# Single root install resolves the whole bun workspace, including the
# testing-platform package, so the separate per-package `npm ci` steps
# (extension + webview-ui + testing-platform) collapse into one.
- name: Install workspace dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci
run: bun install --frozen-lockfile
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
# @cline/* are local workspace symlinks to source packages; build dist/
# before compiling the standalone core.
- name: Build SDK packages
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci
run: bun run build:sdk
- name: Assert better-sqlite3 native binary present
run: |
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
if [ ! -f "$NODE_FILE" ]; then
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
echo "(bun trustedDependencies postinstall likely did not run)"
exit 1
fi
echo "Found better-sqlite3 native binary: $NODE_FILE"
- name: Download ripgrep binaries
run: npm run download-ripgrep
run: bun run download-ripgrep
- name: Compile Standalone
run: npm run compile-standalone
- name: Install testing platform dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/testing-platform ci
run: bun run compile-standalone
- name: Running testing platform integration spec tests
timeout-minutes: 7
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
run: bun run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
@@ -0,0 +1,60 @@
# Some coding-agent GitHub Apps advertise themselves by auto-commenting on every
# new PR ("<Tool> Agent can help with this pull request. Just @<tool> ..."). The
# app needs pull_requests:write for its real job (pushing branches, opening PRs),
# and GitHub offers no per-behavior control over an installed App, so the ad
# cannot be disabled at the source. This deletes those promo comments as they
# appear. Genuine agent output comments (work results, reviews) don't match the
# promo pattern and are left alone.
#
# No checkout, API-calls-only — comment text is only ever handled as data inside
# the script, never interpolated into the workflow definition.
name: repo-delete-agent-promo-comments
on:
issue_comment:
types: [created]
jobs:
delete:
runs-on: ubuntu-latest
timeout-minutes: 2
# Prefilter so a runner only spins up for bot comments that look like the
# ad; the script re-verifies before deleting.
if: >-
github.event.issue.pull_request &&
endsWith(github.event.comment.user.login, '[bot]') &&
contains(github.event.comment.body, 'can help with this pull request')
# Comment deletion goes through the issues API, but GitHub gates the
# endpoint by where the comment lives: issue comments need `issues`,
# PR-conversation comments need `pull-requests`. The prefilter restricts
# this job to PR comments, so pull-requests is the one that matters;
# issues is kept in case the prefilter is ever widened.
permissions:
issues: write
pull-requests: write
steps:
# Pinned to a commit SHA (not the mutable v7 tag) because this job holds
# write permissions and fires on attacker-postable events.
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const comment = context.payload.comment
// Belt and suspenders on top of the job-level prefilter: only
// delete when the author is a real GitHub App bot AND the body
// matches the self-promotion shape ("... can help with this
// pull request. Just @<handle> ..."). A human quoting the ad
// text is not a Bot; a bot posting real work output doesn't
// match the promo shape.
const isBot = comment.user.type === "Bot"
const isPromo = /\bcan help with this pull request\b[\s\S]*@\w/i.test(comment.body || "")
if (!isBot || !isPromo) {
core.info("not an agent promo comment, leaving it alone")
return
}
await github.rest.issues.deleteComment({
...context.repo,
comment_id: comment.id,
})
core.info(`deleted promo comment ${comment.id} by ${comment.user.login} on #${context.payload.issue.number}`)
@@ -0,0 +1,65 @@
# Cloud coding agents append promotional badge blocks to PR bodies after the
# agent's final turn, wrapped around <!-- <VENDOR>_AGENT_PR_BODY_BEGIN/END -->
# marker comments. The agent itself never sees that content, so no repo rule or
# agent instruction can prevent it. This strips it from the PR description on
# open/edit, keeping only the agent-authored content between the markers.
#
# Uses pull_request_target so the token has write access on PRs from forks. That
# trigger is only unsafe when a job checks out and executes PR code — this one
# never checks out the repository, it only calls the REST API.
name: repo-strip-agent-badges
on:
pull_request_target:
types: [opened, edited]
concurrency:
group: strip-agent-badges-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
strip:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.body, '_AGENT_PR_BODY')
permissions:
pull-requests: write
steps:
# Pinned to a commit SHA (not the mutable v7 tag) because this job holds
# write permissions under pull_request_target.
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
// Re-fetch instead of trusting the event payload: the body may have
// been edited again between the event firing and this run (agent
// harnesses edit PR bodies post-open), and updating from the stale
// snapshot would clobber the newer content.
const { data: pr } = await github.rest.pulls.get({
...context.repo,
pull_number: context.payload.pull_request.number,
})
const body = pr.body || ""
// The BEGIN/END comments wrap the agent-authored content; everything
// outside them (vendor promo badges, "open in <tool>" links) is
// appended by the harness. Keep only what's between the markers.
// The backreference requires BEGIN and END to name the same vendor.
// No markers -> no match -> body passes through unchanged.
const cleaned = body
.replace(
/^[\s\S]*?<!--\s*([A-Z][A-Z0-9_]*?)_AGENT_PR_BODY_BEGIN\s*-->\r?\n?([\s\S]*?)<!--\s*\1_AGENT_PR_BODY_END\s*-->[\s\S]*$/,
"$2",
)
.trimEnd()
// No change means a previous run already cleaned this body. Returning
// without an update is what stops `edited` from retriggering forever.
if (cleaned === body) {
core.info("nothing to strip")
return
}
await github.rest.pulls.update({
...context.repo,
pull_number: pr.number,
body: cleaned,
})
core.info(`stripped ${body.length - cleaned.length} chars from PR #${pr.number}`)
+58
View File
@@ -260,6 +260,41 @@ jobs:
git push origin "refs/tags/${TAG}"
done
- name: Get Previous SDK Tag
if: steps.check_commits.outputs.skip != 'true' && steps.channel.outputs.channel == 'latest'
id: prev_tag
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
# The checkout is shallow and tagless, so fetch the release tags explicitly.
git fetch origin "+refs/tags/sdk/sdk/v*:refs/tags/sdk/sdk/v*"
PREV_TAG=$(git tag -l 'sdk/sdk/v*' | grep -vx "sdk/sdk/v${VERSION}" | sort -V | tail -1 || echo "")
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
- name: Get Changelog Entry
if: steps.check_commits.outputs.skip != 'true' && steps.channel.outputs.channel == 'latest'
id: changelog
run: |
# Grab content between the first "## " header and the next one in sdk/CHANGELOG.md
CONTENT=$(awk '/^## [0-9]/{if(found) exit; found=1; next} found{print}' sdk/CHANGELOG.md)
DELIMITER=$(openssl rand -hex 8)
echo "content<<${DELIMITER}" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "${DELIMITER}" >> $GITHUB_OUTPUT
- name: Create GitHub Release
if: steps.check_commits.outputs.skip != 'true' && steps.channel.outputs.channel == 'latest'
uses: softprops/action-gh-release@v1
with:
tag_name: sdk/sdk/v${{ steps.version.outputs.version }}
name: "SDK v${{ steps.version.outputs.version }}"
body: |
${{ steps.changelog.outputs.content }}
${{ steps.prev_tag.outputs.prev_tag != '' && format('**Full Changelog**: https://github.com/{0}/compare/{1}...sdk/sdk/v{2}', github.repository, steps.prev_tag.outputs.prev_tag, steps.version.outputs.version) || '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
env:
@@ -280,3 +315,26 @@ jobs:
echo " - sdk/core/v${VERSION}"
echo " - sdk/sdk/v${VERSION}"
fi
- name: Post release to Slack
if: steps.check_commits.outputs.skip != 'true' && steps.channel.outputs.channel == 'latest'
uses: slackapi/slack-github-action@v3.0.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
payload: |
channel: "C0APVKGGZFC"
text: "Cline SDK v${{ steps.version.outputs.version }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "Cline SDK v${{ steps.version.outputs.version }}"
- type: "section"
text:
type: "mrkdwn"
text: ${{ toJSON(steps.changelog.outputs.content) }}
- type: "context"
elements:
- type: "mrkdwn"
text: "<https://www.npmjs.com/package/@cline/sdk/v/${{ steps.version.outputs.version }}|View on npm>${{ steps.prev_tag.outputs.prev_tag != '' && format(' | Full Changelog: https://github.com/{0}/compare/{1}...sdk/sdk/v{2}', github.repository, steps.prev_tag.outputs.prev_tag, steps.version.outputs.version) || '' }}"
+149
View File
@@ -0,0 +1,149 @@
name: ui-publish
on:
workflow_dispatch:
inputs:
npm_tag:
description: "npm distribution tag"
required: true
type: choice
options:
- next
- latest
default: next
confirm_publish:
description: 'Type "publish" to publish @cline/ui to npm'
required: true
type: string
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
quality:
name: UI quality and package checks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Typecheck UI
run: bun -F @cline/ui typecheck
- name: Test UI
run: bun -F @cline/ui test
- name: Build Storybook
run: bun -F @cline/ui build-storybook
- name: Build UI package
run: bun -F @cline/ui build
# The desktop chat test imports @cline/shared/browser, which resolves to
# dist output that nothing else in this job builds.
- name: Build shared package
run: bun -F @cline/shared build
- name: Test desktop chat integration
run: bun -F @cline/code test:chat-ui
- name: Pack publish artifact
id: pack
shell: bash
run: |
set -euo pipefail
pack_dir="$RUNNER_TEMP/ui-npm-pack"
mkdir -p "$pack_dir"
cd sdk/packages/ui
bun pm pack --ignore-scripts --destination "$pack_dir" --quiet
archive=$(find "$pack_dir" -maxdepth 1 -name '*.tgz' -print -quit)
test -n "$archive"
echo "archive=$archive" >> "$GITHUB_OUTPUT"
- name: Test packed package
env:
UI_PACKAGE_ARCHIVE: ${{ steps.pack.outputs.archive }}
run: bun sdk/packages/ui/scripts/smoke-package.ts "$UI_PACKAGE_ARCHIVE"
- name: Upload publish artifact
uses: actions/upload-artifact@v4
with:
name: ui-npm-package
path: ${{ runner.temp }}/ui-npm-pack/*.tgz
if-no-files-found: error
retention-days: 7
publish:
name: Publish @cline/ui
if: >-
github.event_name == 'workflow_dispatch' &&
github.repository == 'cline/cline' &&
github.ref == 'refs/heads/main' &&
inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
needs: quality
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Download publish artifact
uses: actions/download-artifact@v4
with:
name: ui-npm-package
path: ${{ runner.temp }}/ui-npm-pack
- name: Verify publish tooling
shell: bash
run: |
set -euo pipefail
npm_version=$(npm --version)
echo "npm ${npm_version}"
node -e 'const [major, minor, patch] = process.argv[1].split(".").map(Number); if (major < 11 || (major === 11 && (minor < 5 || (minor === 5 && patch < 1)))) { console.error("npm 11.5.1 or newer is required for trusted publishing"); process.exit(1); }' "$npm_version"
- name: Publish package
shell: bash
env:
NPM_CONFIG_PROVENANCE: "true"
NPM_TAG: ${{ inputs.npm_tag }}
run: |
set -euo pipefail
archive=$(find "$RUNNER_TEMP/ui-npm-pack" -maxdepth 1 -name '*.tgz' -print -quit)
if [ -z "$archive" ]; then
echo "UI package archive was not downloaded"
exit 1
fi
version=$(tar -xOf "$archive" package/package.json | node -e 'let input=""; process.stdin.on("data", chunk => input += chunk); process.stdin.on("end", () => process.stdout.write(JSON.parse(input).version))')
if npm view "@cline/ui@${version}" version >/dev/null 2>&1; then
echo "@cline/ui@${version} already exists; bump sdk/packages/ui/package.json before publishing"
exit 1
fi
npm publish "$archive" --tag "$NPM_TAG" --access public
echo "Published @cline/ui@${version} with npm tag '${NPM_TAG}'"
+12
View File
@@ -13,6 +13,9 @@ tmp
pnpm-lock.yaml
.clineignore
.cline/enterprise
.cline/remote-config
**/.cline/remote-config
.venv
.actrc
CLAUDE.local.md
@@ -81,3 +84,12 @@ apps/vscode/webview-ui/src/**/*.js.map
*.db-wal
.cline/**/managed.json
.cline/**/bundle.json
apps/vscode/tsconfig.test.generated.json
.next/dev/static
**/src-tauri/target/debug/.fingerprint
apps/examples/desktop-app/src-tauri/target
apps/examples/desktop-app/webview/.next
# Next.js generated type shim (churns between dev and build)
apps/examples/desktop-app/webview/next-env.d.ts
apps/examples/desktop-app/.cursor/settings.json
-8
View File
@@ -39,14 +39,6 @@
"sdk/packages/core/src/auth/**"
],
"severity": "high"
},
{
"id": "sdk-telemetry-doc-update",
"rule": "Any PR that adds new event constants to CORE_TELEMETRY_EVENTS in packages/core/src/services/telemetry/core-events.ts, adds new capture* helper functions, or changes the payload shape of an existing event must update the Event Catalog section in DOC.md. Flag PRs that modify core-events.ts without a corresponding change to DOC.md.",
"scope": [
"sdk/packages/core/src/services/telemetry/core-events.ts"
],
"severity": "medium"
}
]
}
+1 -5
View File
@@ -16,13 +16,9 @@
"path": "sdk/packages/core/src/services/telemetry/OpenTelemetryProvider.ts",
"description": "OpenTelemetry-backed provider that wires logs/metrics/traces exporters. Contains createConfiguredTelemetryService and createConfiguredTelemetryHandle, the canonical factories every host should use."
},
{
"path": "DOC.md",
"description": "Public API and event documentation. The Event Catalog and 'Activation funnel' sections must be kept in sync with core-events.ts. Host integration rules (CLI dir ordering, hub daemon metadata forwarding) are documented here."
},
{
"path": "sdk/ARCHITECTURE.md",
"description": "Architecture reference. Telemetry design decisions, completion semantics (submit_and_exit anchoring), and hub-daemon telemetry forwarding are documented here. Use as ground truth for design intent."
"description": "Architecture reference. Telemetry design decisions and completion semantics (submit_and_exit anchoring) are documented here. Use as ground truth for design intent."
},
{
"path": "sdk/AGENTS.md",
+22 -17
View File
@@ -36,8 +36,13 @@ event names. It exports:
1. Add the constant to `CORE_TELEMETRY_EVENTS`
2. Add a typed `capture*()` helper alongside it (with a typed `properties` parameter)
3. Update the Event Catalog section in `DOC.md`
4. Add a unit test in `core-events.test.ts` asserting the event is dropped when telemetry is opted out
3. Add a unit test in `core-events.test.ts` asserting the event flows through the
opt-out-respecting `capture` path and never `captureRequired` (opt-out is enforced by
`OptedOutTelemetryService`, whose `capture` is a no-op — the test convention is
"emits X as a normal opt-out-respecting event"). Events that intentionally bypass
opt-out must use `captureRequired` and assert that explicitly.
**All events should be named using snake_case and so should their properties**
## The Activation Funnel
@@ -82,7 +87,7 @@ The CLI accepts `--config <dir>`. The CLI **must** apply `setClineDir(...)` and
and any other on-disk telemetry state lands under `~/.cline` instead of the user's chosen
config dir.
The canonical pattern is in `apps/cli/src/main.ts` (PR #357):
The canonical pattern is in `apps/cli/src/main.ts`:
```ts
if (configDir) setClineDir(configDir);
@@ -90,18 +95,18 @@ setHomeDir(homedir());
captureCliExtensionActivated(); // <-- after dir overrides
```
## Hub Daemon Metadata Forwarding
## Hub Daemon Telemetry
Hosts that spawn a detached `@cline/core/hub/daemon-entry` process must forward telemetry
metadata into the daemon argv so the daemon can reconstruct an equivalent
`ITelemetryService`. The expected payload is base64-encoded JSON with snake_case keys:
The detached hub daemon (`sdk/packages/core/src/hub/daemon/entry.ts`) hosts the
`LocalRuntimeHost` that emits `task.conversation_turn` and `task.tokens` for every
hub-backed session, so the daemon must own its own `ITelemetryService`. It builds one via
`createHubDaemonTelemetry()` (`sdk/packages/core/src/hub/daemon/telemetry.ts`), which
identifies from the cached cline account (re-resolved periodically, since the daemon often
starts before login) and flushes on every shutdown path, including startup failure.
```
{ extension_version, cline_type, platform, platform_version, os_type, os_version, is_remote_workspace }
```
The reference implementation is `apps/vscode/src/hub-daemon.ts` (PR #357). Without this
forwarding, hub-backed sessions silently drop their lifecycle telemetry.
Flag changes that remove this wiring, construct runtime hosts inside the daemon without
passing its telemetry handle, or add daemon exit paths that skip the flush — hub-backed
sessions would silently drop their lifecycle telemetry (this exact bug shipped once).
## Auth Lifecycle Completeness
@@ -120,10 +125,10 @@ canonical examples of all four phases.
## Single Telemetry Service Per Host
On VS Code, the telemetry handle is built **once** in `activate()`
(`apps/vscode/src/telemetry.ts`) and the same instance is passed into the sidebar, panel
command, and daemon spawn payload. Do not let individual controllers construct their own
`ITelemetryService` — that fragments distinct-id state, opt-out tracking, and flush ownership.
On VS Code, all callers go through the lazy `telemetryService` proxy in
`apps/vscode/src/services/telemetry/index.ts`, which constructs the service once on first
use. Do not let individual controllers construct their own `ITelemetryService` — that
fragments distinct-id state, opt-out tracking, and flush ownership.
The CLI follows the same pattern via the `getCliTelemetryService()` singleton in
`apps/cli/src/utils/telemetry.ts`, which is memoized by the activation gate in
+2 -1
View File
@@ -7,4 +7,5 @@ fi
gitleaks git --pre-commit --redact --staged --verbose || exit 1
lint-staged
cd apps/vscode && bunx lint-staged
+6 -7
View File
@@ -51,7 +51,8 @@
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
"CLINE_ENVIRONMENT": "staging"
"CLINE_ENVIRONMENT": "staging",
"CLINE_DIR": "${userHome}/.cline_staging"
}
},
{
@@ -75,7 +76,8 @@
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
"CLINE_ENVIRONMENT": "local"
"CLINE_ENVIRONMENT": "local",
"CLINE_DIR": "${userHome}/.cline_local"
}
},
{
@@ -126,10 +128,7 @@
"${workspaceFolder}/apps/vscode/dist-standalone/**/*.js"
],
"preLaunchTask": "compile-standalone",
"runtimeExecutable": "npx",
"runtimeArgs": [
"tsx"
],
"runtimeExecutable": "bun",
"program": "scripts/test-standalone-core-api-server.ts",
"envFile": "${workspaceFolder}/apps/vscode/.env",
"env": {
@@ -183,7 +182,7 @@
"name": "Open Storybook",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeExecutable": "bun",
"runtimeArgs": [
"run",
"storybook"
+14 -1
View File
@@ -22,11 +22,24 @@
},
// Enable Lint and format using Biome
"biome.enabled": true,
"biome.requireConfiguration": true,
"prettier.enable": false,
"editor.defaultFormatter": "biomejs.biome",
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.removeUnused.biome": "always",
"source.removeUnusedImports": "always",
"source.organizeImports.biome": "always"
},
// Disable auto-forwarding ports to prevent Simple Browser from opening the Vite dev server
+39 -19
View File
@@ -6,7 +6,7 @@
{
"label": "compile-standalone",
"type": "shell",
"command": "npm run compile-standalone",
"command": "bun run compile-standalone",
"group": "build",
"problemMatcher": [],
"presentation": {
@@ -19,7 +19,7 @@
{
"label": "npm: protos",
"type": "shell",
"command": "npm run protos",
"command": "bun run protos",
"problemMatcher": [],
"isBackground": false,
"presentation": {
@@ -65,10 +65,10 @@
},
{
"type": "shell",
"command": "npm run build:webview",
"command": "bun run build:webview",
"group": "build",
"problemMatcher": [],
"isBackground": true,
"isBackground": false,
"label": "npm: build:webview",
"dependsOn": [
"npm: protos"
@@ -86,10 +86,10 @@
},
{
"type": "shell",
"command": "npm run build:webview:test",
"command": "bun run build:webview:test",
"group": "build",
"problemMatcher": [],
"isBackground": true,
"isBackground": false,
"label": "npm: build:webview:test",
"dependsOn": [
"npm: protos"
@@ -108,22 +108,22 @@
},
{
"type": "shell",
"command": "npm run dev:webview",
"command": "bun run dev:webview",
"group": "build",
"problemMatcher": [
{
"pattern": [
{
"regexp": ".",
"regexp": "^(?!)((?:.*))$",
"kind": "file",
"file": 1,
"location": 2,
"message": 3
"message": 1
}
],
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "."
"beginsPattern": "^Building webview for|^\\s*VITE",
"endsPattern": "^.*Local:\\s+http://127\\.0\\.0\\.1:[0-9]+/"
}
}
],
@@ -145,7 +145,7 @@
},
{
"type": "shell",
"command": "npm run watch:esbuild",
"command": "bun run watch:esbuild",
"group": "build",
"problemMatcher": {
"pattern": [
@@ -169,7 +169,8 @@
"isBackground": true,
"label": "npm: watch:esbuild",
"dependsOn": [
"npm: protos"
"npm: protos",
"build-sdk:debug"
],
"presentation": {
"group": "watch",
@@ -184,7 +185,7 @@
},
{
"type": "shell",
"command": "npm run watch:esbuild:test",
"command": "bun run watch:esbuild:test",
"group": "build",
"problemMatcher": {
"pattern": [
@@ -208,7 +209,8 @@
"isBackground": true,
"label": "npm: watch:esbuild:test",
"dependsOn": [
"npm: protos"
"npm: protos",
"build-sdk:debug"
],
"presentation": {
"group": "watch",
@@ -224,7 +226,7 @@
},
{
"type": "shell",
"command": "npm run watch:tsc",
"command": "bun run watch:tsc",
"group": "build",
"problemMatcher": "$tsc-watch",
"isBackground": true,
@@ -242,7 +244,7 @@
},
{
"type": "shell",
"command": "npm run watch-tests",
"command": "bun run watch-tests",
"label": "npm: watch-tests",
"problemMatcher": "$tsc-watch",
"isBackground": true,
@@ -282,7 +284,7 @@
},
{
"type": "shell",
"command": "npm run storybook",
"command": "bun run storybook",
"group": "build",
"problemMatcher": [],
"isBackground": false,
@@ -311,6 +313,24 @@
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "build-sdk:debug",
"type": "shell",
"command": "bun run build:sdk",
"problemMatcher": [
"$tsc"
],
"presentation": {
"group": "watch",
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}",
"env": {
"CLINE_SOURCEMAPS": "1"
}
}
}
],
"inputs": [
+32
View File
@@ -0,0 +1,32 @@
This is the **Cline** monorepo. Toolchain is **Bun 1.3.13** (package manager + task runner) with **Node >=22** as the runtime. Do not use npm/yarn/pnpm.
## Cloud Agent Instructions
### Cline CLI
- Run from source: `bun run cli` (interactive: `bun run cli -i`; one-shot: append a prompt). This resolves to `apps/cli` and **auto-spawns the `@cline/cline-hub` daemon** — you do not start the hub separately.
- Inspect local health with `bun run cli doctor`; `bun run cli version` prints the version.
- An actual agent turn requires an **LLM provider credential**. With no credentials the default `cline` provider fails fast with an `Unauthorized` error and the interactive TUI shows a provider sign-in screen. Configure via `cline auth` or provider env vars (e.g. `ANTHROPIC_API_KEY`, `CLINE_API_KEY`, `OPENROUTER_API_KEY`); see `apps/cli/README.md`.
### Build / Lint / test
- SDK packages (`@cline/shared|llms|agents|core|sdk`) resolve each other through compiled `dist/` (their `exports` point only at `dist/`, with no `development` source condition). You **must** run `bun run build:sdk` after changing SDK dependencies/source before running the CLI or SDK tests, otherwise imports fail with missing `@cline/*` / missing `dist/` errors. Running processes do **not** hot-reload SDK source changes — rebuild and restart.\
- Known cloud-env test artifact: `@cline/core` test `src/services/workspace/workspace-manifest.test.ts > readGitWorkspaceState > prefers origin and returns the current branch` fails because cloud VMs configure git `insteadOf` rules that rewrite GitHub remotes to `https://x-access-token:...@github.com/...`. This is an environment artifact, not a code bug.
- Some `@cline/cli` e2e assertions (`bun -F @cline/cli test:e2e`) may fail on exact tool-listing string formats; treat as pre-existing test drift, not an environment problem.
### GUI display
- A virtual X display is live at **`DISPLAY=:1`** (the same desktop used for screenshots). GUI apps (VS Code, the Tauri desktop window) launched with `DISPLAY=:1` render there and can be screenshotted — no need to start your own `xvfb`. Prefer starting long-running GUI/dev processes in a `tmux` session (see the tmux guidance) so they survive.
### VS Code extension (`apps/vscode`, package `claude-dev`)
Toolchain is pre-installed and persisted in the VM: generated gRPC/proto code, the bundled `ripgrep` binaries (`apps/vscode/bin/`), the built webview (`webview-ui/build`), the esbuild bundle (`dist/extension.js`), VS Code itself (`/usr/bin/code`), and the GUI system libraries its tests need.
- **Codegen prerequisite:** `bun run protos` (from `apps/vscode`) regenerates `src/generated/*` and the webview grpc client. The `dev`, `build:webview`, and `check-types` scripts already run it, so proto changes are picked up by those commands; run it manually only if you edit `.proto` files without a full build.
- **Build:** `bun run build:webview` (webview UI, ~15s) then `bun esbuild.mjs` (extension bundle). `bun run package` does the full production build.
- **Run it (dev host):** `DISPLAY=:1 code --no-sandbox --user-data-dir=/tmp/vscode-userdata --extensionDevelopmentPath=/workspace/apps/vscode <some-folder>`, then click the Cline icon in the Activity Bar to open the webview. (`--no-sandbox` is required in this container.)
- **Test:** `bun run test:unit` (bun-based, ~984 tests, no VS Code host needed). `bun run test:integration` (`@vscode/test-electron`, downloads a VS Code build, runs under the GUI libs) and `bun run test:e2e` (Playwright) exercise a real extension host — heavier, and the GUI libs for them are already installed.
- One-time deps (already installed, listed here in case they must be recreated): ripgrep via `bun run download-ripgrep`; VS Code test GUI libs per `CONTRIBUTING.md` (`libnss3`, `libatk*`, `libgbm1`, `xvfb`, etc.).
### Desktop app (`apps/examples/desktop-app`, package `@cline/code`)
A Tauri v2 (Rust) shell + Next.js webview + a Bun "sidecar" backend. Rust and the Tauri Linux system libs are pre-installed and persisted.
- **Headless (no Rust/window):** run the backend and UI separately — `bun run dev:sidecar` (Bun backend on `127.0.0.1:3126`, serves `ws://.../transport`) and `bun run dev:web` (Next.js UI on `http://localhost:3125`).
- **Native window:** `bun run dev` (`tauri dev`) — its `beforeDevCommand` builds the sidecar binary and starts `dev:web` (`:3125`), then Rust `main.rs` spawns the sidecar; so free ports `3125`/`3126` first. Launch with `DISPLAY=:1` to see the window. A `libEGL: DRI3 error` warning is benign (software rendering) — the WebKitGTK window still renders.
- **Rust version caveat:** the crate graph needs Cargo's `edition2024` feature, so **Rust ≥1.85** is required (the VM's base 1.83 fails with "feature `edition2024` is required"). The toolchain here was updated via `rustup default stable` (currently 1.97). First `cargo` build downloads/compiles the full Tauri crate graph (a few minutes); subsequent builds are cached.
- **System libs (already installed):** `libwebkit2gtk-4.1-dev`, `libgtk-3-dev`, `libayatana-appindicator3-dev`, `librsvg2-dev`, `libxdo-dev`, `libssl-dev`, `build-essential`.
- **Test/typecheck:** `bun run typecheck`, `bun run test:chat-ui` (Vitest). Both trigger `build:ui` first.
+237
View File
@@ -1,5 +1,242 @@
# Changelog
## [4.1.6]
### Added
- Offer `meta/muse-spark-1.2-contributor` on the Cline provider, alongside a refreshed model catalog.
### Fixed
- Attribute error telemetry to the model actually in use for a run, so failures are no longer reported against the wrong model.
## [4.1.5]
### Added
- Explain when a free model promotion ends. Requests to a retired free model now show a dedicated notice with a button to pick another model, instead of a generic error with nothing but a Retry prompt.
### Changed
- Map reasoning settings onto a shared path across AI SDK providers, so effort levels and enable/disable toggles behave consistently (including on Ollama) instead of relying on per-provider overrides.
## [4.1.4]
### Added
- Recognize Chutes as a provider.
- Show skills alongside workflows in the slash command menu, and disambiguate commands that share a name instead of letting one shadow the other.
### Changed
- Remove model-initiated plan-to-act switching. Switching out of plan mode is now driven by you, not by the model deciding mid-turn.
- Hard-block file-editing shell commands in plan mode instead of relying on prompting alone. Read-only investigation still works, but file manipulation, in-place editors, redirection to files, mutating git subcommands, and package installs are refused.
### Fixed
- Stop treating a turn that completes with a plan as a failed turn when a plan-blocked command was its only tool call. The turn no longer ends in the error state with a Retry footer, and toggling to Act correctly re-runs the presented plan instead of appearing to do nothing.
- Show tool paths relative to the workspace in the chat view instead of absolute paths.
- Reset pending attachments when starting a new task, so images from the previous task no longer carry over.
- Surface a clear error when the selected provider has no API key configured, instead of a generic failure.
- Refresh MCP tool and resource lists when a server sends a `list_changed` notification, instead of only showing a toast.
- Show installed plugins under their real package names instead of all appearing as "index".
- Correct the Linux keybinding label in the Plan/Act mode tooltip.
- Recover from running out of context instead of failing with a raw provider error — the run compacts and retries once, and the cases that genuinely cannot be recovered explain why.
- Retry empty model responses on every provider rather than only Ollama, fixing hard "Model returned empty response" failures on OpenRouter, Cline, and OpenAI-compatible endpoints.
- Stop Claude 4.6+ and 5.x models being rejected with "thinking.type.enabled is not supported" when they resolve from the offline catalog or from a hand-typed model id.
- Restore Bedrock prompt caching, which reported zero cache reads and writes because the provider sent a cache format Bedrock discards, and route Bedrock foundation models through geo inference profiles.
- Send `max_completion_tokens` for reasoning models on OpenAI-compatible endpoints, and substitute image content for models without image support instead of failing the request.
- Inherit the MiniMax default model from models.dev, and refresh the bundled catalog, which adds Infomaniak and SCX.ai.
- Report the same provider failure once instead of twice in error telemetry, and rate-limit repeated failures from unattended retry loops.
## [4.1.3]
### Fixed
- Stop the two bundles of the combined rollout package from invalidating each other's Cline account session. A still-open legacy window that refreshed its token after the machine was promoted to the new extension would consume the shared refresh token, producing spurious "Unauthorized" / re-authenticate prompts and unexpected sign-outs. Promoted legacy windows now keep working on their current session and offer a one-time Reload Window prompt instead.
- Fall back to the default Cline model when migrating a setup that references a model id the new extension doesn't recognize, instead of leaving the provider unconfigured.
- Restore reliable checkpoints: checkpoints are created consistently, and restoring one now rewinds the whole workspace rather than a subset of files.
- Keep settings edits that are made before the provider config finishes loading — base URLs, API keys, and the Qwen/Moonshot API line are no longer silently discarded.
- Stop losing keystrokes in custom base URL fields, and keep the custom URL checkbox state after a failed clear.
- Use the AskSage custom API URL at inference time instead of ignoring it.
- Settle a pending tool approval when an edited message replaces the session, so the task no longer hangs waiting on a prompt that is gone.
- Drop attachments from messages that have been edited.
- Complete terminal commands when the shell execution ends, so tasks no longer stall on commands that already finished.
- Include untracked files when generating commit messages.
- Run Windows Store PowerShell profiles correctly.
- Surface the upstream provider error when a gateway-forwarded stream fails, instead of a generic failure.
- Retry empty Ollama responses at the model boundary, and raise the response-start timeout to 5 minutes so cold model loads no longer error out.
- Show proper display names for Cline free models and recommended models in the model picker.
- Preserve video input capability for models that support it.
- Keep the plan/act input border in sync with the actual textarea focus.
## [4.1.2]
### Added
- Show which extension variant is active — "Legacy" or "Next" — next to the version in the settings About page, in both bundles of the combined rollout package.
## [4.1.1]
### Changed
- Remove vestigial MCP server-key machinery from McpHub — native MCP tool calls now route by server name instead of a random in-memory uid, so routing survives restarts and server list changes.
## [4.1.0]
### Changed
- Convert the stable extension to a combined A/B package: one VSIX containing both the current (legacy) extension and the new SDK-based extension, plus a loader that activates exactly one per window via a staged remote rollout. For nearly all users nothing changes — the loader activates the same extension as 4.0.12; a small percentage (starting at 1%) is gradually opted into the SDK-based extension. If the new extension fails to activate, the loader falls back to the current one in the same window. Settings and credentials are shared between the two.
## [4.0.12]
### Added
- Add support for free Cline models, shown as "(free)" in the model picker, with a dedicated error card that includes the reset time when the free limit is reached.
### Fixed
- Keep Claude Code responses that were already streamed when the CLI exits with a max-turns error, instead of discarding a valid response.
## [4.0.11]
### Added
- Add Claude Opus 5 across the Anthropic, Claude Code, Bedrock, Vertex, Cline, and OpenRouter providers, including 1M context window variants.
- Add Moonshot Kimi K3 support.
- Include the host plugin version in telemetry events.
### Fixed
- Correct pricing for the Claude Opus 1M context variants, which overstated costs for requests above 200k tokens.
- Enable native tool calling for Kimi K3 models, fixing empty responses.
## [4.0.10]
### Added
- Add telemetry to track when Cline reaches the consecutive mistake limit.
## [4.0.9]
### Added
- Add GPT-5.6 ChatGPT subscription models.
### Changed
- Soften and shorten the message shown when Cline hits the consecutive mistake limit.
### Fixed
- Handle cumulative usage snapshots from OpenAI-compatible providers so token counts are no longer over-reported.
- Load skills from files saved as UTF-8 with a byte-order mark (BOM).
## [4.0.8]
### Added
- Add more models to the GCP Vertex provider, plus a free-form entry option in the model dropdown for specifying custom Vertex models.
## [4.0.7]
### Added
- Add a ClinePass limit-reached error with a one-click option to switch to Cline usage-based billing.
- Allow selecting Cline free models on the ClinePass provider, organized into Subscribed and Free tabs with model descriptions.
### Changed
- Refine ClinePass onboarding and provider settings copy, and open the "learn more" link via the in-app URL handler.
- Remove the Cline model picker recommendation copy.
### Removed
- Remove all references to GLM 5.1.
## [4.0.6]
### Fixed
- Generalize the model capability warning so it applies more broadly.
## [4.0.5]
### Added
- Add support for Claude Sonnet 5 across the Anthropic, Bedrock, Vertex, Claude Code, SAP AI Core, OpenRouter, and Vercel AI Gateway providers, including model picker and recommended-model updates.
## [4.0.4]
### Changed
- Fully remove the ClinePass feature flag so ClinePass is available everywhere in the UI — onboarding, settings, the welcome promo banner, and the credit-limit "Switch to ClinePass" action.
## [4.0.3]
### Changed
- Enable the ClinePass provider for all users by removing the feature-flag gate that previously fell back to the standard Cline provider.
## [4.0.2]
### Added
- Add reasoning effort support (including `xhigh`) for DeepSeek thinking models.
- Improve the ClinePass provider experience with clearer reasoning controls and model selection.
### Fixed
- Show reasoning effort controls for ClinePass models and align ClinePass model resolution with the rest of the provider.
- Prefer canonical Cline Z.ai model ids and polish ClinePass and Z.ai model metadata.
- Fix environment variable replacement in the webview.
- Default focus chain settings in webview state so the toggle reflects the correct value on load.
## [4.0.1]
### Changed
- Roll the stable VS Code extension back to the pre-SDK-migration codebase to resolve regressions reported in 4.0.0. This release ships the 3.89.2 extension code under a higher version number so existing 4.0.0 users receive the update. SDK-migration work continues separately on `main`.
## [4.0.0]
### Added
- Add the SDK-backed VS Code extension runtime. Cline now runs tasks through the shared Cline SDK session layer for agent turns, tools, Plan/Act mode coordination, MCP, checkpoints, telemetry, provider changes, compaction, mistake limits, and task history.
- Add ClinePass to the VS Code extension, including onboarding, provider selection, signup and subscription handoff, live model lists, entitlement and organization error states, out-of-credit prompts, and clearer ClinePass auth/error handling.
- Add the Customize marketplace for discovering and managing Skills, MCP servers, and Plugins from the extension, including installed/marketplace tabs, search and filtering, install/uninstall flows, enable/disable controls, and support for plugin-bundled skills.
- Cline Plugins: Plugins let you extend Cline with custom tools, workflows, skills, and MCP-powered capabilities tailored to your team or project. Install them from the new Customize marketplace to add specialized behavior, connect external services, and package reusable automations—so Cline can do more than code: it can adapt to the way you work.
- Add queued prompts in chat. Messages submitted while Cline is already working are now queued, shown while the current turn streams, and can be cancelled before they run.
- Add edit-and-regenerate support for previous user messages, with clearer Reset Chat and Reset Code actions.
- Add generic SDK provider settings and model-catalog support so more providers can share the same model picker, reasoning controls, dynamic model IDs, provider config persistence, and custom model handling.
- Add additional SDK-backed provider exposure and model/provider updates, including ClinePass models, refreshed Cline catalog data, Fireworks GLM 5.2, Kimi K2.6 Fast, Kimi K2.7 Code, Qwen 3.7 Plus, MiniMax M3 updates, SAP AI Core wiring, LiteLLM model fetching, Codex OAuth credentials, and OpenAI-compatible model settings.
- Add MCP support for plugins and shared marketplace install/uninstall plumbing used by the VS Code extension.
### Changed
- Migrate the VS Code extension from the legacy task implementation to the shared Cline SDK and move the extension build/package workflow to Bun.
- Rework Plan/Act mode handling through SDK coordinators, including closer CLI parity and automatic continuation when switching from Plan to Act.
- Rework provider and model configuration around `providers.json`, the model catalog, and SDK session config so settings are preserved consistently across provider switches and active sessions can restart when the selected provider changes.
- Simplify provider settings UI by replacing many provider-specific views with shared generic settings components and consistent reasoning selectors.
- Simplify terminal execution through the SDK run-commands path, including clearer non-interactive command guidance and safer structured command formatting.
- Migrate legacy MCP files and formats into the shared settings file and protect MCP settings writes with safer locking/atomic updates.
- Refresh the MCP hub automatically after marketplace installs so newly installed servers are available without a manual restart.
- Reorganize MCP/Skills/Plugins entry points under Customize, hide workflows from the Customize menu, wrap Customize tabs on narrow screens, and allow the MCP Marketplace tab to be disabled remotely while installed MCP servers remain accessible.
- Simplify auto-approval settings. Command auto-approval is now disabled by default for safer new and reset configurations, and the auto-approval UI has been streamlined.
- Update task history handling for the SDK migration, including legacy task history visibility, metadata preservation on resume, and corrected deletion behavior.
- Route compacting and mistake-limit behavior through the SDK so the Compact button and mistake tracking affect the active SDK session.
- Remove the legacy Explain Changes feature as part of the SDK migration cleanup.
- Temporarily disable subagents in the VS Code extension while the SDK-backed experience is stabilized.
### Fixed
- Fix marketplace edge cases, including refreshing MCP servers after marketplace installs, disabling the MCP Marketplace tab from remote config, hiding workflows from Customize, surfacing plugin-bundled skills, and uninstalling shared marketplace entries.
- Fix chat submission during active turns by queuing user messages instead of dropping or racing them, showing pending/queued states promptly, rendering direct user messages immediately, and removing delayed send behavior.
- Fix editing previous user messages so Escape cancels editing locally and reset action labels are clearer.
- Fix terminal reliability, including standalone Windows output capture, hardened PowerShell command handling, running-state display for in-progress commands, raw structured command preservation, single-quote handling, cwd setup timeouts, failing-command stdout capture, heredoc coalescing, and removal of duplicated command echoes in tool results.
- Fix SDK tool-result and provider-message budgeting by truncating large tool outputs by default, capping assistant text, limiting bash/file-read/search output ingestion, bounding media budgets, batching outdated-read rewrites to preserve provider prefix caches, and normalizing JSON-like tool inputs by schema.
- Fix login and feature-flag resolution by using the correct user/account identity on startup and simplifying the login UX.
## [3.89.2]
### Fixed
-2
View File
@@ -1,2 +0,0 @@
@.clinerules/general.md
@.clinerules/network.md
+15 -15
View File
@@ -7,7 +7,7 @@ We're thrilled you're interested in contributing to Cline. Whether you're fixing
Bug reports help make Cline better for everyone! Before creating a new issue, please [search existing ones](https://github.com/cline/cline/issues) to avoid duplicates. When you're ready to report a bug, head over to our [issues page](https://github.com/cline/cline/issues/new/choose) where you'll find a template to help you with filling out the relevant information.
<blockquote class='warning-note'>
🔐 <b>Important:</b> If you discover a security vulnerability, please use the <a href="https://github.com/cline/cline/security/advisories/new">Github security tool to report it privately</a>.
🔐 <b>Important:</b> If you discover a security vulnerability, please use the <a href="https://github.com/cline/cline/security/advisories/new">GitHub security tool to report it privately</a>.
</blockquote>
@@ -45,7 +45,7 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
3. Install [bun](https://bun.com)
4. Install the necessary dependencies for the extension and webview-gui:
```bash
cd apps/vscode && npm run install:all && cd ../..
cd apps/vscode && bun run install:all && cd ../..
cd sdk && bun run build && cd ..
```
5. Generate Protocol Buffer files (required before first build):
@@ -61,8 +61,8 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
2. Push your branch and create a PR on GitHub. Our CI will:
- Run tests and checks
3. Testing
- Run `cd apps/vscode && npm run test` to run tests locally.
- Before submitting PR, run `npm run format:fix` to format your code
- Run `cd apps/vscode && bun run test` to run tests locally.
- Before submitting PR, run `bun run format:fix` to format your code
### Extension
@@ -74,12 +74,12 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
2. **Local Development**
- cd into the vscode extension, `cd apps/vscode`
- Run `npm run install:all` to install dependencies
- Run `npm run protos` to generate Protocol Buffer files (required before first build)
- Run `npm run test` to run tests locally
- Run `bun run install:all` to install dependencies
- Run `bun run protos` to generate Protocol Buffer files (required before first build)
- Run `bun run test` to run tests locally
- Run → Start Debugging or `>Debug: Select and Start Debugging` and wait for a new VS Code instance to open
- **Terminal Workflow**: Use `npm run dev` (generates protos + runs watch mode) or `npm run watch` (if protos already generated)
- Before submitting PR, run `npm run format:fix` to format your code
- **Terminal Workflow**: Use `bun run dev` (generates protos + runs watch mode) or `bun run watch` (if protos already generated)
- Before submitting PR, run `bun run format:fix` to format your code
3. **Linux-specific Setup**
VS Code extension tests on Linux require the following system libraries:
@@ -135,8 +135,8 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
2. **Code Quality**
- Run `npm run lint` to check code style
- Run `npm run format` to automatically format code
- Run `bun run lint` to check code style
- Run `bun run format` to automatically format code
- All PRs must pass CI checks which include both linting and formatting
- Address any warnings or errors from linter before submitting
- Follow TypeScript best practices and maintain type safety
@@ -144,7 +144,7 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
3. **Testing**
- Add tests for new features
- Run `npm test` to ensure all tests pass
- Run `bun test` to ensure all tests pass
- Update existing tests if your changes affect them
- Include both unit tests and integration tests where appropriate
@@ -154,9 +154,9 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
- **Running E2E tests:**
```bash
npm run test:e2e # Build and run all E2E tests
npm run e2e # Run tests without rebuilding
npm run test:e2e -- --debug # Run with interactive debugger
bun run test:e2e # Build and run all E2E tests
bun run e2e # Run tests without rebuilding
bun run test:e2e -- --debug # Run with interactive debugger
```
- **Writing E2E tests:**
+5 -5
View File
@@ -149,7 +149,7 @@ Toggle between Plan mode and Act mode. In Plan mode, Cline explores your codebas
## Rules and Skills
Define project-specific rules in `.clinerules` files that guide how Cline works in your codebase: coding standards, architecture conventions, deployment procedures, testing requirements. Rules are picked up automatically by the CLI, VS Code extension, and JetBrains plugin. Use skills to let the model load specific rules when needed.
Define project-specific rules in `.clinerules` files that guide how Cline works in your codebase: coding standards, architecture conventions, deployment procedures, testing requirements. Rules are picked up automatically by the CLI, VS Code extension, and JetBrains plugin. Use skills to let the model load specific rules when needed.
## Works With Every Model
@@ -158,10 +158,10 @@ Cline is not locked to a single AI provider. Use whichever model fits your workf
| Provider | Models |
|----------|--------|
| Anthropic | Claude Opus, Sonnet, Haiku |
| OpenAI | GPT series model |
| Google | Gemini series model |
| OpenAI | GPT series models |
| Google | Gemini series models |
| OpenRouter | 200+ models from any provider |
| Vercel AI Gateway | Models through Vercel AI Gateway |
| Vercel AI Gateway | Route to many providers through one gateway |
| AWS Bedrock | Claude, Llama, and more |
| Azure / GCP Vertex | All hosted models |
| Cerebras / Groq | Fast inference models |
@@ -226,7 +226,7 @@ Run Cline with zero interaction for scripting and automation. Pipe input, get JS
```bash
cline "Run tests and fix any failures"
git diff origin/main | cline "Review these changes for issues"
git diff origin/main | cline "Review these changes for issues"
cline --json "List all TODO comments" | jq -r 'select(.type == "agent_event" and .event.text) | .event.text'
```
+228
View File
@@ -1,5 +1,233 @@
# Cline CLI Changelog
## 3.0.52
- Added `cline mcp uninstall` for removing an installed MCP server
- Schedules now reuse your saved provider settings instead of needing provider configuration of their own
- Queued messages are legible on light-theme terminals — they were previously rendered in a color that washed out against a light background
- MCP tool results render as readable text in the TUI instead of escaped JSON, and binary payloads survive being expanded instead of being mangled
- Malformed tool input/output payloads no longer break rendering — the formatters degrade gracefully instead of throwing
- Prompts queued during a turn now survive being interrupted: they are preserved across aborts, drained after a turn aborts itself, and the stop is surfaced instead of leaving the queue silently dropped (from SDK v0.0.72)
- Session context stays durable across aborts and hub restarts, so an interrupted session resumes with the state it had (from SDK v0.0.72)
- A hung MCP server no longer takes down session creation, and stdio servers that were never configured get a 30-second initialize budget instead of blocking indefinitely (from SDK v0.0.72)
- Remote SSE MCP servers surface an OAuth authorization prompt on a 401 instead of failing outright, and pre-registered OAuth clients are supported for setups without dynamic client registration (from SDK v0.0.72)
- LiteLLM requests route through Chat Completions instead of the Responses API, fixing calls against LiteLLM proxies (from SDK v0.0.72)
- Network interruptions that happen mid-stream but before any model output are retried instead of failing the turn (from SDK v0.0.72)
- Vertex ADC token refreshes use the configured fetch, so they work behind proxies and custom transports (from SDK v0.0.72)
- Checkpoint diffs include files that were untracked when the snapshot was taken, and checkpoints are picked up when git is initialized part-way through a session (from SDK v0.0.72)
- Scheduled run reports carry execution context — readable headers, schedule metadata, durations, and lifecycle error details (from SDK v0.0.72)
## 3.0.51
- Reasoning effort now applies consistently across providers instead of going through per-provider thinking overrides, including Ollama, and asking for reasoning to be off is respected everywhere (from SDK v0.0.71)
- `meta/muse-spark-1.2-contributor` is now selectable on the Cline provider, alongside a refreshed model catalog (from SDK v0.0.71)
- Error telemetry now reports the model that was actually in use for the run (from SDK v0.0.71)
## 3.0.50
- Added user-selectable color themes to the interactive TUI. Pick one with `/theme`, the command palette, or the Theme row in `/settings` — the picker previews each theme live. Built-in themes are Auto (terminal-adaptive, the default), Cline Dark, Cline Light, Tokyo Night, Gruvbox Dark, Nord, Dracula, Catppuccin Mocha, One Dark, Solarized Dark, and Solarized Light. Named themes paint the background, foreground, accents, syntax highlighting, and diff colors, and `CLINE_THEME` overrides the persisted choice at startup
- The git branch shown below the prompt now updates when you switch branches from another terminal or your editor, instead of showing whatever was checked out when the TUI started
- Telegram slash commands such as `/clear` now reach the connector command host — the Telegram library was intercepting them and they were silently dropped
- Racing connector launches no longer collide: an instance is claimed before it opens socket mode, the hub supervises connector processes, and `doctor`/`connect` skip connectors that are already starting. Connector tools are also enabled by default, and the Slack greeting is no longer replayed on reconnect
- Auto-approval settings are now honored over ACP
- Plan mode now hard-blocks file-editing shell commands instead of relying on prompting alone — `run_commands` stays available for read-only investigation, but file-manipulation commands, in-place editors (`sed -i`, `perl -i`), redirection to files, mutating git subcommands, package installs, and nested command strings (`sh -c`, `eval`, `sudo`) are rejected, on Windows and PowerShell too (from SDK v0.0.70)
- A turn that ends with a completed plan is no longer rendered as a failed turn when a plan-blocked command was its only tool call
- Running out of context is now recovered from instead of failing with a raw provider error: the run force-compacts and retries once, and the cases that genuinely cannot be recovered report why (from SDK v0.0.70)
- Empty model responses are now retried on every provider, not just Ollama — OpenRouter, Cline, and OpenAI-compatible endpoints previously failed the task outright with "Model returned empty response" (from SDK v0.0.70)
- Claude 4.6+ and 5.x models are no longer rejected with "thinking.type.enabled is not supported" when they resolve from the offline catalog or from a hand-typed model id (from SDK v0.0.70)
- Bedrock prompt caching works again — the provider was sending a cache format Bedrock silently discards, so cache reads and writes were always 0 — and Bedrock foundation models are now routed through geo inference profiles (from SDK v0.0.70)
- Reasoning models on OpenAI-compatible endpoints now receive `max_completion_tokens` instead of the rejected `max_tokens`, and requests to models without image support substitute the image content instead of failing (from SDK v0.0.70)
- MiniMax now inherits its default model from models.dev, and the model catalog picked up two new providers, Infomaniak and SCX.ai (from SDK v0.0.70)
- Upgraded the model layer to AI SDK 7 and switched Ollama to the native AI SDK provider (from SDK v0.0.70)
- Error telemetry no longer reports the same provider failure twice, and repeated failures from unattended retry loops are rate-limited (from SDK v0.0.70)
## 3.0.49
- `/undo` works again once the agent has used tools — the checkpoint picker counted tool results as user turns, so restore aborted with "Could not find user message for run N"
- Checkpoints are actually created again; a run-boundary regression meant none were ever recorded in the CLI (from SDK v0.0.69)
- Checkpoint restore is now a full workspace rewind: files Cline created during the task come back at their checkpoint-time content and files created after the checkpoint are removed, while `.gitignore`d paths (build output, `node_modules`, `.env`) are left alone (from SDK v0.0.69)
- After a restore, the rewound message is prefilled as plain text instead of the raw `<user_input mode="act">` envelope
- Ollama's response-start timeout is now 5 minutes instead of 30 seconds, so cold-loading a large local model no longer errors out mid-load (from SDK v0.0.69)
- Empty Ollama responses are now retried instead of failing the task with "Model returned empty response" (from SDK v0.0.69)
- Migrated users whose stored Cline model id isn't in the catalog now fall back to the default model instead of sending an unknown model id on every request (from SDK v0.0.69)
- The ClinePass promo dialog can be dismissed with any key (Enter still opens the subscription page), and it is marked as shown when it appears, so force-quitting no longer replays it on every launch
- Opening a URL no longer crashes the CLI on hosts without an opener binary (headless Linux without `xdg-open`); WSL2 containers now use `xdg-open`, Windows tries the absolute PowerShell path first, and `cline doctor log` converts Linux paths to `\\wsl$` UNC paths
- The hub now restarts through the installed wrapper after a Unix self-update, so npm cannot reuse a deleted cached executable
- ACP: ClinePass is selectable as a provider, organizations can be selected, session resolution and text rendering on session restart are fixed, and agent errors now describe the actual failure
- Provider errors forwarded through the Vercel AI Gateway now surface the real upstream message instead of a raw Zod dump or `[object Object]` (from SDK v0.0.68)
- Cline free models and recommended models now show their real display names in the model picker (from SDK v0.0.68)
- Sessions rooted at the filesystem root (`/`) no longer fail every command (from SDK v0.0.68)
- On Windows, PowerShell commands now travel over UTF-8 stdin, so non-ASCII commands survive the active code page and long commands are not capped by the command-line limit (from SDK v0.0.68)
- The live model catalog no longer drops the video input capability (from SDK v0.0.68)
- Removed the CLI promo code flow
## 3.0.48
- `cline history` now opens inside the existing TUI, with resume and delete actions, instead of rendering a second view in the same process
- Connector threads (Slack, Discord, Telegram, Linear, Google Chat, WhatsApp) now recover when the session they were bound to is gone — the stale binding is dropped and the turn replays against a new session, instead of failing with "session not found" until `threads.json` is edited by hand
- `cline --help` now reports the real default `--config` and `--data-dir` paths
- The per-server `timeout` in `cline_mcp_settings.json` is now honored for `initialize`, `tools/list`, and `tools/call`, so slow MCP servers no longer fail against a hardcoded 5s limit (from SDK v0.0.67)
- Reasoning controls are now routed from the models.dev catalog across providers, with clamped budgets and correct per-provider encoding (from SDK v0.0.67)
- OpenRouter now defaults to `anthropic/claude-sonnet-5` (from SDK v0.0.67)
- Fixed the China and international endpoint toggles being ignored for Qwen, Moonshot, and Z AI (from SDK v0.0.67)
- Legacy API keys are now migrated for every secret-backed provider (from SDK v0.0.67)
- Legacy OpenAI Compatible model-info overrides now survive into the seeded `models.json` (from SDK v0.0.67)
- Fixed auto-compaction state being rejected as stale, which added a redundant summarizer call on every turn past the compaction trigger (from SDK v0.0.67)
- Fixed checkpoint restores across session resumes (from SDK v0.0.67)
- Tool calls that pass line numbers as strings (`insert_line`, `read_files` bounds) are now accepted instead of erroring (from SDK v0.0.67)
- A legacy single-file `.clinerules` no longer aborts the config scan (from SDK v0.0.67)
- Plugins can now emit telemetry through `ctx.telemetry` (from SDK v0.0.67)
## 3.0.47
- Free Cline models are now supported end to end: free models show as "(free)", and hitting the free limit renders a dedicated card with the reset time (from SDK v0.0.66)
- `/settings` general toggles (plan/act mode, tool auto-approve, compaction mode) now persist across restarts
- Upgraded the TUI stack from opentui 0.1.102 to 0.4.3
- Fixed a grey panel left behind on screen after closing a dialog (model picker, help, command palette) — a leftover from the opentui upgrade
- Fixed a React duplicate-key warning when `read_files` listed the same path more than once
- Aborting a task no longer risks killing the shared hub daemon
- Connector status delivery failures are no longer fatal to the turn
- Agentic compaction is now the default context-compaction strategy, with fixes for it silently falling back to basic compaction and for tool-heavy transcripts that could never find a cut point (from SDK v0.0.66)
- Editor edits preserve a file's existing line endings, fixing failed exact-match edits on CRLF files (from SDK v0.0.66)
- Broader built-in provider coverage, now generated from models.dev (from SDK v0.0.66)
- Updated the bundled model catalog (from SDK v0.0.66)
## 3.0.46
- Fixed out-of-credits detection so the CLI reliably recognizes the Cline API's real `insufficient_credits` (402) error and shows the "add credits" card instead of a generic error
## 3.0.45
- Smaller install: the Claude Code and Codex providers are now optional and loaded on demand, cutting `npm i -g cline` from ~640MB to ~285MB (from SDK v0.0.65)
- Kimi K3 is now available as a ClinePass model (from SDK v0.0.65)
- Runs now retry once after refreshing expired OAuth credentials (from SDK v0.0.65)
- Team runs: the spawn tool is no longer exposed to teammates, and errored teammate runs now report as failed instead of completed (from SDK v0.0.65)
- Hub status output now includes version numbers
- Updated the bundled model catalog (from SDK v0.0.65)
## 3.0.44
- Improved max output token handling across providers (gateway routing, OpenAI vendor, and reasoning models) (from SDK v0.0.64)
- Frontmatter and configuration files that start with a UTF-8 byte order mark (e.g. saved by Windows editors) now parse correctly (from SDK v0.0.64)
## 3.0.43
- The CLI now automatically trusts your operating system's certificate store, so it works behind corporate proxies and TLS-inspecting firewalls without manually setting `NODE_EXTRA_CA_CERTS` (fixes "unable to get local issuer certificate" errors, including Windows intermediate CA stores)
## 3.0.42
- Fixed Ollama native API routing so context window and timeout settings work again
## 3.0.41
- Compaction now shows progress status in the TUI
- Model IDs are now suggested from OpenAI-compatible endpoints when configuring a provider
- Workspace git info (branch/remote) is now persisted and refreshed across sessions
- Compaction no longer runs during an active turn
- Fixed a crash when the terminal title was updated during TUI teardown
- The API key fallback hint is now highlighted for better visibility
- Benign git states are no longer reported as workspace initialization errors
## 3.0.40
- Added a manual API key escape hatch for Cline OAuth providers, so you can enter a key by hand from settings
- Fixed provider config not reloading when switching models
- Fixed auto-update failing to detect Bun global installs after symlink resolution
- Fixed unexpected logouts caused by transient network or server errors during token refresh
- The ClinePass usage-limit error is now surfaced clearly when you hit the limit
- Session id is now preserved when continuing within the same session
- Hardened context compaction budget handling
## 3.0.39
- You can now select Cline free models on the ClinePass provider in the model picker
- Removed the retired ClinePass GLM 5.1 model
- Fixed OpenAI Codex model metadata under the GPT Subscription provider
- `str_replace` edits now report accurate diffs
- Fixed context compaction so canonical session history is preserved
- The detached hub daemon now emits telemetry, and telemetry identity now includes `user_id`
- Cline provider requests now send versioned client-identity headers
## 3.0.38
- New plan/act accent palette: act mode is now blue (`#79b8ff`) and plan mode amber, replacing the old cyan/yellow — applied across dialogs, the model selector, config, onboarding, markdown, and syntax highlighting, with light-theme variants tuned for contrast
- Restyled chat input: a minimal frame with full-width horizontal rules and a bold accent prompt glyph instead of the tinted background, plus slimmer user-message bubbles
- Assistant markdown accents are now tinted by the mode (plan/act) they were produced in
- Polished the status bar usage display and ClinePass model name
- Harmonized the success/diff green and dark syntax-highlighting colors with the new brand palette
- The thinking-level picker now defaults its cursor to Medium instead of Off
- `read_files` now tolerates malformed input from weaker models: line-range entries (`start_line`/`end_line`) sent as separate array items are coalesced back onto the preceding file path instead of being rejected (from SDK v0.0.58)
- Models in the live catalog that don't report a context window now default to a 128K input-token limit, so under-specified models get a usable context budget (from SDK v0.0.57)
## 3.0.37
- Weaker models (e.g. DeepSeek) that emit malformed tool calls — wrong argument types or truncated JSON — are now handled gracefully and run instead of erroring out
- Plan/act mode switches are now visible to the model, so it knows when you change modes mid-session
- Fixed plan/act mode notices being dropped from prompts sent to the model
- Fixed a race where switching modes in an empty session could trigger an unexpected restart
## 3.0.36
- Fixed plan mode's `switch_to_act_mode` tool not taking effect until the end of the turn: the model would keep running with plan-mode tools (no file editor) and fall back to editing files through shell commands. Switching to act mode now ends the plan-mode run and automatically continues with the approved plan using the full act-mode toolset. A Tab mode toggle racing a completing turn can no longer auto-start plan execution you didn't approve.
## 3.0.35
- ClinePass is now enabled for all CLI users
- Recover missing interactive sessions when reading messages
- Format structured commands in history export
- Add the subscription promo code when linking to the dashboard subscription page
- Add Tencent TokenHub as a provider (from SDK v0.0.55)
- Fix first-prompt truncation on high-output models (e.g. MiniMax M3) that could immediately auto-compact and cut the initial task down to just the input wrapper (from SDK v0.0.55)
- Use a curated default when migrating legacy provider settings (from SDK v0.0.55)
- Advertise run commands as shell strings (from SDK v0.0.55)
- Refresh the bundled model catalog with the latest provider models (from SDK v0.0.55)
## 3.0.34
- Fixed the ClinePass upgrade notice appearing immediately after completing onboarding.
- Improved the wording of the ClinePass onboarding step.
- Streamlined the Cline provider picker by merging the subscription and usage/billing options into one and removing the credits link.
## 3.0.33
- Show a ClinePass subscription URL as a fallback during onboarding so you can still subscribe if the subscription screen can't open automatically
- Hide the ClinePass promo for users who already have a ClinePass subscription
- Use an adaptive plan accent color for ClinePass prompts so they fit the active theme
## 3.0.32
- Improved the ClinePass onboarding experience
- Added an intermediate step before going to ClinePass model selection
- Made the ClinePass subscription screen selectable
- Promoted ClinePass in the startup notice
- Used "ClinePass" as one word consistently and refined the provider UI copy
- More accurate context compaction and clearer error messages (from SDK v0.0.54)
## 3.0.31
- Show when request cost is covered by your Cline subscription
- Prompt to switch to ClinePass when you run out of credits, and list ClinePass features in the not-subscribed message
- Added an option to open the subscription page from the ClinePass options
- Added marketplace uninstall support and surfaced plugin-bundled skills
- Require quoted prompts for one-shot mode
- Capped MCP tool names at 64 characters for OpenAI-compatible providers
- Updated coupon code
## 3.0.30
- Added a token count to the status bar, shown alongside cost
- Added organization-specific error messages
- Added SAP AI Core provider support
- Refreshed the model catalog with the latest provider models
- Preserved OpenRouter reasoning-disable behavior and improved OpenRouter prompt caching
- Routed LiteLLM model fetches through the SDK and stopped unrelated models from appearing in the LiteLLM model list
- Updated ClinePass models live, restored ClinePass models in onboarding, and improved ClinePass error messages
- Threaded proxy/CA-aware networking into the inference path
- Persisted Bedrock settings to providers.json
- Normalized JSON-like tool inputs by schema for more reliable tool calls
- Fixed an "ERROR: EMPTY CONTENT" message that could appear when an error occurred
- Fixed a packaging issue (createRequire) that could break the CLI at runtime
## 3.0.29
- Costs are now hidden for Cline free models
+31
View File
@@ -339,6 +339,9 @@ bun run test:e2e:interactive
# TUI-specific E2E tests (uses @microsoft/tui-test)
bun run test:e2e:cli:tui
# TUI E2E tests driven through tuistory (PTY + Ghostty terminal emulator)
bun run test:e2e:tuistory
# Type checking
bun run typecheck
@@ -364,6 +367,34 @@ bun run dev -- --interactive --config /tmp/cline-test
Or set `CLINE_FORCE_ONBOARDING=1` to force the onboarding view regardless of existing config.
### Manually testing the TUI (agents / headless environments)
[tuistory](https://github.com/remorses/tuistory) is installed as a devDependency. It wraps the TUI in a named background PTY session that can be scripted from a plain shell — no real terminal or display needed. This is the preferred way for AI agents (or anyone in a headless environment) to poke at the interactive TUI:
```bash
cd apps/cli
# Launch the TUI in a background session
bunx tuistory -s cline --cols 120 --rows 36 -- bun src/index.ts --provider anthropic -m claude-sonnet-4-6 -k test-key
# Wait reactively for the chat view (no sleep guessing)
bunx tuistory -s cline wait "What can I do for you?" --timeout 30000
# Interact and inspect
bunx tuistory -s cline type "/settings"
bunx tuistory -s cline press enter
bunx tuistory -s cline snapshot --trim # current screen as text
bunx tuistory -s cline screenshot # current screen as a styled PNG
# A human can watch/drive the same session from another terminal
tuistory attach -s cline
# Tear down
bunx tuistory -s cline close
```
The same engine powers the `test:e2e:tuistory` vitest suite (`src/cli.tuistory.e2e.test.ts`), which uses the programmatic `launchTerminal()` API for assertions against the emulated screen.
### Adding a new TUI component
1. Create a `.tsx` file in `src/tui/components/`
+21 -4
View File
@@ -221,13 +221,15 @@ In chat surfaces, connector slash commands include `/help`, `/start`, `/new`, `/
Schedule agents on cron-like intervals or external events.
If `--provider` and `--model` are omitted, schedules use the last configured
provider and model. If only `--provider` is given, the schedule uses that
provider's saved model.
```sh
cline schedule create "Daily code review" \
--cron "0 9 * * MON-FRI" \
--prompt "Review PRs opened yesterday and summarize issues." \
--workspace /path/to/repo \
--provider cline \
--model openai/gpt-5.3-codex \
--timeout 3600 \
--tags automation,review
@@ -257,10 +259,10 @@ Schedules can route results back to chat surfaces with `--delivery-adapter`, `--
| `--hooks-dir <path>` | Additional hooks directory hint for runtime hook injection |
| `--acp` | ACP (Agent Client Protocol) mode |
| `--thinking [none\|low\|medium\|high\|xhigh]` | Model thinking level when supported. Defaults to `medium` when the flag is provided without a level; thinking is off when the flag is omitted. |
| `--compaction <agentic\|basic\|off>` | Context compaction mode. Defaults to `basic`; use `agentic` for LLM compaction or `off` to disable. |
| `--compaction <agentic\|basic\|off>` | Context compaction mode. Defaults to `agentic`; use `basic` for local truncation or `off` to disable. |
| `--retries <count>` | Maximum consecutive mistakes (retries) before halting (default: `3`) |
| `--json` | Output NDJSON instead of styled text |
| `--data-dir <path>` | Use isolated local state at `<path>` instead of `~/.cline` (enables sandbox mode automatically) |
| `--data-dir <path>` | Use isolated local state at `<path>` instead of `~/.cline/data` (enables sandbox mode automatically) |
| `--auto-approve [true\|false]` | Set tool auto-approval for all tools |
| `--kanban` | Run the external `kanban` app |
| `-y, --yolo` | Skip tool approval prompts, enable `submit_and_exit`, and disable spawn/team tools by default |
@@ -346,9 +348,24 @@ Desktop-integrated approval mode is also supported via env wiring (`CLINE_TOOL_A
- `CLINE_LOG_LEVEL` - Runtime log level (`trace|debug|info|warn|error|fatal|silent`, default `info`)
- `CLINE_LOG_PATH` - Runtime log file path (default `<CLINE_DATA_DIR>/logs/cline.log`)
- `CLINE_LOG_NAME` - Logger name embedded in runtime log records
- `CLINE_DEBUG` - Set to `1`/`true` to print wrapper diagnostics (e.g. the CA bundle summary)
`--key` takes precedence over environment variables.
## Certificate trust
The CLI automatically trusts your operating system's certificate store, so it
works behind corporate TLS-inspecting proxies and with self-signed/internal
endpoints without any setup. On launch the `cline` wrapper harvests the OS trust
anchors and writes them to `~/.cline/cli-node-extra-ca-certs.pem`, then points
the runtime's `NODE_EXTRA_CA_CERTS` at that bundle. The file is regenerated when
it changes and is safe to delete (it is rebuilt on the next run).
If you set `NODE_EXTRA_CA_CERTS` yourself, your certificates are **merged** into
that bundle alongside the system store rather than replacing it. Run with
`CLINE_DEBUG=1` to see how many OS and user CAs were loaded and where the bundle
was written.
## Contributing
See [DEVELOPMENT.md](./DEVELOPMENT.md) for local development setup, monorepo structure, and TUI architecture. See [DISTRIBUTION.md](./DISTRIBUTION.md) for how the CLI is packaged and distributed.
+281
View File
@@ -0,0 +1,281 @@
// Auto-discovery of OS trust anchors for the Cline CLI.
//
// Bun does not read the OS trust store, so the 3.x CLI cannot see corporate
// MITM / self-signed CAs out of the box. This runs in the Node `bin/cline`
// wrapper (not Bun), reads the full OS store via tls.getCACertificates("system")
// (Node >= 22, no --use-system-ca flag), and hands the certs to the Bun child
// via NODE_EXTRA_CA_CERTS, which both runtimes honor. Mirrors the JetBrains
// plugin's configureCertificates(), sourcing from the OS instead of the IDE.
//
// Dependency-free CommonJS with injectable modules so it is unit-testable and
// ships verbatim in the published wrapper package.
const PEM_MARKER = "-----BEGIN CERTIFICATE-----";
const CERT_BLOCK =
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
/**
* Returns only the complete certificate blocks from PEM text, or null when
* there are none. User files may also hold private keys (combined cert+key
* PEMs) or other sections, which must never be copied into the managed
* bundle. Files that contain nothing but certificates pass through verbatim
* so unchanged bundles keep hash-skipping the rewrite.
*/
function sanitizePem(text) {
const blocks = text.match(CERT_BLOCK) ?? [];
if (blocks.length === 0) {
return null;
}
const rest = text.replace(CERT_BLOCK, "");
if (/^\s*$/.test(rest)) {
return text;
}
return `${blocks.join("\n")}\n`;
}
/**
* Returns OS-trusted certificates as PEM strings, or [] when unavailable.
* tls.getCACertificates("system") requires Node >= 22.
*/
function harvestSystemCerts(tlsModule) {
try {
const tls = tlsModule || require("node:tls");
if (typeof tls.getCACertificates !== "function") {
return [];
}
const certs = tls.getCACertificates("system");
if (!Array.isArray(certs)) {
return [];
}
return certs.filter(
(cert) => typeof cert === "string" && cert.includes(PEM_MARKER),
);
} catch {
return [];
}
}
/**
* Returns the file's certificate blocks as PEM text, or null when missing,
* unreadable, or holding no complete certificate block.
*/
function readUserBundle(fsModule, userPath) {
if (!userPath) {
return null;
}
try {
const fs = fsModule || require("node:fs");
const stat = fs.statSync(userPath, { throwIfNoEntry: false });
if (!stat || !stat.isFile()) {
return null;
}
// Binary DER would not have loaded in the runtime either; require PEM.
return sanitizePem(fs.readFileSync(userPath, "utf8"));
} catch {
return null;
}
}
/**
* Reads the user's NODE_EXTRA_CA_CERTS value into PEM strings. Node treats the
* value as a single file, but some users set an OS-path-delimited list; the
* whole value is tried as one file first, then split.
* The managed bundle is excluded so reading it back never re-appends its certs.
*/
function readUserCerts(fsModule, pathModule, value, managedPath) {
if (!value) {
return [];
}
const fs = fsModule || require("node:fs");
const path = pathModule || require("node:path");
const candidates = [];
const whole = readUserBundle(fs, value);
if (whole) {
candidates.push({ filePath: value, pem: whole });
} else if (value.includes(path.delimiter)) {
for (const segment of value.split(path.delimiter)) {
const trimmed = segment.trim();
if (!trimmed) {
continue;
}
const pem = readUserBundle(fs, trimmed);
if (pem) {
candidates.push({ filePath: trimmed, pem });
}
}
}
const pems = [];
for (const candidate of candidates) {
const isManaged =
managedPath &&
path.resolve(candidate.filePath) === path.resolve(managedPath);
if (!isManaged) {
pems.push(candidate.pem);
}
}
return pems;
}
/**
* Concatenates the user PEMs (if any) and the system certificates into one
* bundle. A separating newline is inserted between parts so adjacent END/BEGIN
* markers cannot fuse into one invalid line.
*/
function buildBundle({ systemCerts, userPems }) {
const parts = [...(userPems ?? []), ...systemCerts];
return parts
.map((part) => (part.endsWith("\n") ? part : `${part}\n`))
.join("");
}
/** Counts individual PEM certificates across the given bundle strings. */
function countCerts(pems) {
let count = 0;
for (const pem of pems) {
count += pem.split(PEM_MARKER).length - 1;
}
return count;
}
function readFileIfExists(fs, filePath) {
try {
return fs.readFileSync(filePath, "utf8");
} catch {
return null;
}
}
function resolveClineDir(env, os, path) {
return env.CLINE_DIR?.trim() || path.join(os.homedir(), ".cline");
}
/**
* True when the api-unavailable warning should print. Stamped per Node version
* in the cline dir so the nudge shows once rather than on every command; a
* version change (upgrade that still falls short, or downgrade) re-arms it.
* When the stamp cannot be read or written, warn — bookkeeping failures must
* never suppress a real diagnostic.
*/
function shouldWarnApiUnavailable(env, deps = {}) {
const fs = deps.fs || require("node:fs");
const os = deps.os || require("node:os");
const path = deps.path || require("node:path");
const version = deps.nodeVersion || process.versions.node;
const dir = resolveClineDir(env, os, path);
const stamp = path.join(dir, `.ca-api-warned-${version}`);
try {
if (fs.existsSync(stamp)) {
return false;
}
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(stamp, "", { mode: 0o600 });
return true;
} catch {
return true;
}
}
/** Atomically writes [content] to [target]; returns true on success. */
function writeBundle(fs, dir, target, content) {
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
try {
fs.mkdirSync(dir, { recursive: true });
// Owner read/write: the bundle holds public CA material, not secrets,
// but there is no reason to make it world-writable.
fs.writeFileSync(tmp, content, { mode: 0o600 });
try {
fs.renameSync(tmp, target);
} catch {
// Windows can reject rename over a file a concurrent child holds open.
fs.rmSync(target, { force: true });
fs.renameSync(tmp, target);
}
return true;
} catch {
// Never leave a partial temp file behind (e.g. ENOSPC mid-write).
try {
fs.rmSync(tmp, { force: true });
} catch {
// Ignore: best-effort cleanup.
}
return false;
}
}
/**
* Harvests OS trust anchors, merges them with any user NODE_EXTRA_CA_CERTS, and
* points env.NODE_EXTRA_CA_CERTS at a single managed PEM bundle. Mutates `env`
* in place. Returns an outcome the caller can log; `action` is one of
* "unchanged" | "written" | "write-failed-reused" | "write-failed" |
* "no-system-certs" | "api-unavailable".
*/
function configureNodeExtraCaCerts(env, deps = {}) {
const fs = deps.fs || require("node:fs");
const os = deps.os || require("node:os");
const path = deps.path || require("node:path");
const tls = deps.tls || require("node:tls");
// tls.getCACertificates("system") needs Node >= 22.15; on older Nodes the
// harvest cannot run at all, which the caller should surface to the user.
if (typeof tls.getCACertificates !== "function") {
return {
action: "api-unavailable",
path: null,
systemCertCount: 0,
userCertCount: 0,
};
}
const systemCerts = harvestSystemCerts(tls);
if (systemCerts.length === 0) {
// Nothing to add: leave any user-provided NODE_EXTRA_CA_CERTS untouched
// and let the runtime fall back to its bundled CAs.
return {
action: "no-system-certs",
path: null,
systemCertCount: 0,
userCertCount: 0,
};
}
const managedDir = resolveClineDir(env, os, path);
const managedPath = path.join(managedDir, "cli-node-extra-ca-certs.pem");
const userValue = (env.NODE_EXTRA_CA_CERTS || "").trim() || null;
const userPems = readUserCerts(fs, path, userValue, managedPath);
const bundle = buildBundle({ systemCerts, userPems });
const base = {
path: managedPath,
systemCertCount: systemCerts.length,
userCertCount: countCerts(userPems),
};
// Skip the rewrite when the bundle is already current. Avoids per-launch I/O
// and the concurrent-rename race in the steady state.
if (readFileIfExists(fs, managedPath) === bundle) {
env.NODE_EXTRA_CA_CERTS = managedPath;
return { ...base, action: "unchanged" };
}
if (writeBundle(fs, managedDir, managedPath, bundle)) {
env.NODE_EXTRA_CA_CERTS = managedPath;
return { ...base, action: "written" };
}
// Write failed: fall back to a previously-written bundle if one exists.
if (readFileIfExists(fs, managedPath)) {
env.NODE_EXTRA_CA_CERTS = managedPath;
return { ...base, action: "write-failed-reused" };
}
return { ...base, path: null, action: "write-failed" };
}
module.exports = {
harvestSystemCerts,
sanitizePem,
readUserBundle,
readUserCerts,
buildBundle,
countCerts,
configureNodeExtraCaCerts,
shouldWarnApiUnavailable,
};
+42
View File
@@ -23,6 +23,48 @@ const childEnv = {
CLINE_WRAPPER_PATH: scriptPath,
};
// Auto-discover OS trust anchors and pass them to the Bun child via
// NODE_EXTRA_CA_CERTS. The Bun runtime does not read the OS store on its own,
// so corporate/self-signed CAs would otherwise fail. This wrapper runs on
// Node, which can read the full store here.
try {
const caCerts = require("./ca-certs.cjs");
const outcome = caCerts.configureNodeExtraCaCerts(childEnv);
const debug =
process.env.CLINE_DEBUG === "1" || process.env.CLINE_DEBUG === "true";
// Not debug-gated: on old Nodes the harvest silently doing nothing is
// indistinguishable from a broken corporate proxy. Stamped per Node
// version so the nudge shows once, not on every command.
if (
outcome &&
outcome.action === "api-unavailable" &&
!childEnv.NODE_EXTRA_CA_CERTS &&
caCerts.shouldWarnApiUnavailable(childEnv)
) {
console.warn(
`[cline] Node ${process.versions.node} cannot read the OS trust store (needs >= 22.15); ` +
"corporate or self-signed CAs may fail TLS. Upgrade Node or set NODE_EXTRA_CA_CERTS.",
);
}
if (debug && outcome) {
if (outcome.action === "no-system-certs") {
console.warn(
"[cline] No OS trust anchors found; relying on the runtime's bundled CAs.",
);
} else if (outcome.action === "write-failed") {
console.warn(
"[cline] Could not write the managed CA bundle; relying on the runtime's bundled CAs.",
);
} else {
console.warn(
`[cline] Trust: ${outcome.systemCertCount} OS + ${outcome.userCertCount} user CAs (${outcome.action}) -> ${outcome.path}`,
);
}
}
} catch {
// Best effort: fall back to the runtime's default trust on any failure.
}
function run(target) {
const result = childProcess.spawnSync(target, process.argv.slice(2), {
stdio: "inherit",
+1 -1
View File
@@ -121,7 +121,7 @@ const result = await Bun.build({
},
env: "OTEL_*",
banner:
'import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
'import { createRequire as __clineCreateRequire } from "node:module"; const require = __clineCreateRequire(import.meta.url);',
});
if (result.logs.length > 0) {
+9 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@cline/cli",
"displayName": "cline",
"version": "3.0.29",
"version": "3.0.52",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"type": "module",
"publishConfig": {
@@ -62,6 +62,7 @@
"test:unit": "vitest run --config vitest.config.ts",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:e2e:interactive": "vitest run --config vitest.interactive.e2e.config.ts",
"test:e2e:tuistory": "vitest run --config vitest.tuistory.e2e.config.ts",
"test:watch": "vitest --config vitest.config.ts",
"test:e2e:cli:tui": "cd src/tests && tui-test",
"link": "bun unlink && bun link"
@@ -78,19 +79,19 @@
"@cline/cline-hub": "workspace:*",
"@gramio/format": "^0.7.0",
"@opentui-ui/dialog": "^0.1.2",
"@opentui/core": "0.1.102",
"@opentui/react": "0.1.102",
"@opentui/core": "0.4.3",
"@opentui/react": "0.4.3",
"chat": "^4.23.0",
"commander": "^14.0.3",
"fzf": "^0.5.2",
"marked": "^15.0.12",
"open": "^10.2.0",
"opentui-spinner": "^0.0.6",
"opentui-spinner": "^0.0.7",
"pino": "^10.3.1",
"posthog-node": "^5.8.0",
"react": "19.2.4",
"react-devtools-core": "^7.0.1",
"react-reconciler": "0.32.0",
"react-reconciler": "0.33.0",
"yaml": "^2.8.2",
"nanoid": "^5.1.7",
"zod": "^4.1.11"
@@ -99,8 +100,9 @@
"@cline/core": "workspace:*",
"@cline/shared": "workspace:*",
"@microsoft/tui-test": "^0.0.2",
"@types/bun": "^1.3.10",
"@types/react": "19.2.14",
"vitest": "^4.0.18",
"@types/bun": "^1.3.10"
"tuistory": "^0.10.1",
"vitest": "^4.0.18"
}
}
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# Launch the Cline CLI in ACP mode from source, for use as a Zed custom agent.
#
# Zed spawns agents without your interactive shell's PATH, so `bun` (installed
# via mise/asdf/nvm/homebrew) is usually not resolvable. This wrapper finds bun
# explicitly and execs it from the repo root.
#
# IMPORTANT: stdout is the JSON-RPC channel. Never echo to stdout here — any
# stray byte corrupts the ACP stream. Diagnostics go to stderr.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
# Prefer an explicit override, then PATH, then common version-manager locations.
if [ -n "${BUN_BIN:-}" ]; then
bun_bin="$BUN_BIN"
elif command -v bun > /dev/null 2>&1; then
bun_bin="$(command -v bun)"
else
bun_bin=""
for candidate in \
"$HOME"/.local/share/mise/installs/bun/*/bin/bun \
"$HOME"/.bun/bin/bun \
/opt/homebrew/bin/bun \
/usr/local/bin/bun; do
if [ -x "$candidate" ]; then
bun_bin="$candidate"
break
fi
done
fi
if [ -z "$bun_bin" ]; then
echo "acp-dev.sh: could not find the 'bun' executable; set BUN_BIN to its path" >&2
exit 127
fi
cd "$REPO_ROOT"
exec "$bun_bin" --conditions=development --cwd apps/cli dev --acp "$@"
+333 -37
View File
@@ -7,6 +7,8 @@ import type {
ContentBlock,
InitializeRequest,
InitializeResponse,
LoadSessionRequest,
LoadSessionResponse,
NewSessionRequest,
NewSessionResponse,
PromptRequest,
@@ -28,11 +30,12 @@ import {
ProviderSettingsManager,
SessionSource,
} from "@cline/core";
import type { Message } from "@cline/shared";
import { isLikelyAuthError, type Message } from "@cline/shared";
import { getPersistedProviderApiKey } from "../commands/auth";
import { resolveSystemPrompt } from "../runtime/prompt";
import { subscribeToAgentEvents } from "../runtime/session-events";
import { createCliCore } from "../session/session";
import { isClineOrgIndividualInferenceSubscriptionErrorMessage } from "../utils/cline-pass-errors";
import { getCliBuildInfo } from "../utils/common";
import { randomSessionId, resolveWorkspaceRoot } from "../utils/helpers";
import type { Config } from "../utils/types";
@@ -43,8 +46,24 @@ import {
authenticateAcpProvider,
isAcpAuthMethodId,
} from "./auth";
import { requestAcpToolApproval } from "./permissions";
import {
AUTO_APPROVE_CONFIG_ID,
buildAutoApproveConfigOption,
parseAutoApproveValue,
} from "./auto-approve";
import {
buildOrganizationConfigOption,
fetchClineOrganizations,
getAcpOrgSubscriptionMessage,
ORGANIZATION_CONFIG_ID,
PERSONAL_ACCOUNT_VALUE,
switchClineOrganization,
usesClineAccount,
} from "./organizations";
import { requestAcpToolApproval } from "./permissions";
import { replaySessionHistory } from "./session-load";
import {
describeAgentError,
forwardAgentEvent,
sendConfigOptionUpdate,
sendCurrentModeUpdate,
@@ -61,6 +80,8 @@ interface SessionState {
currentProviderId: string;
/** Current model id for the session. */
currentModelId: string;
/** When true, all tool calls are approved without asking the client. */
autoApproveTools: boolean;
/** Active session manager for the running agent, if any. */
sessionManager?: ClineCore;
/** Internal session id within the session manager. */
@@ -69,6 +90,15 @@ interface SessionState {
abortController?: AbortController;
/** Unsubscribe function for the agent event listener. */
unsubscribe?: () => void;
/**
* Most recent unrecoverable agent error for the in-flight turn.
*
* The runtime reports fatal failures (bad credentials, subscription
* restrictions, provider outages) as an `error` event and still resolves
* `send()` normally, so the message has to be stashed here for `prompt()` to
* turn into an error response.
*/
fatalError?: Error;
/** Messages to inject into the next session manager for conversation continuity. */
pendingInitialMessages?: Message[];
}
@@ -77,12 +107,17 @@ export class AcpAgent implements Agent {
private sessions = new Map<string, SessionState>();
private readonly conn: AgentSideConnection;
private readonly providerSettingsManager = new ProviderSettingsManager();
private readonly defaultAutoApproveTools: boolean;
/** Set after a successful `authenticate` call. */
private authResult?: AcpAuthResult;
constructor(conn: AgentSideConnection) {
constructor(
conn: AgentSideConnection,
options?: { autoApproveTools?: boolean },
) {
this.conn = conn;
this.defaultAutoApproveTools = options?.autoApproveTools ?? false;
}
async initialize(_params: InitializeRequest): Promise<InitializeResponse> {
@@ -109,7 +144,7 @@ export class AcpAgent implements Agent {
};
}
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
isSessionReady() {
// Require authentication unless an API key is provided via env var.
if (!this.authResult && !process.env.CLINE_API_KEY) {
// Check for valid persisted credentials from a previous session
@@ -119,18 +154,46 @@ export class AcpAgent implements Agent {
if (!this.authResult) {
throw RequestError.authRequired(
undefined,
"Call authenticate before creating a session",
"Call authenticate before starting a session",
);
}
}
}
availableModes() {
return [
{
id: "plan",
name: "Plan",
description:
"Explore the codebase and plan changes without modifying files",
},
{
id: "act",
name: "Act",
description: "Make changes to the codebase",
},
];
}
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
this.isSessionReady();
const sessionId = randomSessionId();
const defaultMode = "act";
const providerId =
process.env.CLINE_PROVIDER ?? this.authResult?.providerId ?? "cline";
const defaultModelId =
process.env.CLINE_MODEL ?? "anthropic/claude-sonnet-4.6";
const providerModels = await Llms.getModelsForProvider(providerId);
// Model ids are provider-scoped, so the default must come from the
// provider's own catalog: `cline-pass` uses `cline-pass/…` ids that mean
// nothing to `cline`, and vice versa.
const defaultModelId = await resolveDefaultModelId(
providerId,
process.env.CLINE_MODEL,
providerModels,
);
this.sessions.set(sessionId, {
id: sessionId,
@@ -139,9 +202,9 @@ export class AcpAgent implements Agent {
currentMode: defaultMode,
currentProviderId: providerId,
currentModelId: defaultModelId,
autoApproveTools: this.defaultAutoApproveTools,
});
const providerModels = await Llms.getModelsForProvider(providerId);
const availableModels = Object.entries(providerModels).map(
([modelId, info]) => ({
modelId,
@@ -150,22 +213,13 @@ export class AcpAgent implements Agent {
}),
);
const organizationOption =
await this.getOrganizationConfigOption(providerId);
return {
sessionId,
modes: {
availableModes: [
{
id: "plan",
name: "Plan",
description:
"Explore the codebase and plan changes without modifying files",
},
{
id: "act",
name: "Act",
description: "Make changes to the codebase",
},
],
availableModes: this.availableModes(),
currentModeId: defaultMode,
},
models: {
@@ -176,10 +230,87 @@ export class AcpAgent implements Agent {
await buildProviderConfigOption(providerId),
buildModelConfigOption(defaultModelId, providerModels),
buildModeConfigOption(defaultMode),
buildAutoApproveConfigOption(this.defaultAutoApproveTools),
...(organizationOption ? [organizationOption] : []),
],
};
}
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
this.isSessionReady();
let session = this.sessions.get(params.sessionId);
let messages: Message[];
if (session?.sessionManager && session.activeSessionId) {
// The session is still live in this connection — replay its current
// conversation without restarting anything.
messages =
(await session.sessionManager.readMessages(session.activeSessionId)) ??
[];
} else {
if (!session) {
// Provider/model are not persisted per session — a session
// loaded on a fresh connection starts from the same defaults
// as a new session, with the model resolved against the
// provider's own catalog just like newSession.
const providerId =
process.env.CLINE_PROVIDER ?? this.authResult?.providerId ?? "cline";
const providerModels = await Llms.getModelsForProvider(providerId);
session = {
id: params.sessionId,
cwd: params.cwd,
mcpServers: params.mcpServers,
currentMode: "act",
currentProviderId: providerId,
currentModelId: await resolveDefaultModelId(
providerId,
process.env.CLINE_MODEL,
providerModels,
),
autoApproveTools: this.defaultAutoApproveTools,
};
this.sessions.set(params.sessionId, session);
}
try {
messages =
(await this.ensureSessionManager(session, params.sessionId, {
resume: true,
})) ?? [];
} catch (error) {
this.sessions.delete(params.sessionId);
throw error;
}
}
// The ACP spec requires the full conversation to be replayed via
// session/update notifications before this request resolves.
await replaySessionHistory(this.conn, params.sessionId, messages);
const providerModels = await Llms.getModelsForProvider(
session.currentProviderId,
);
const availableModels = Object.entries(providerModels).map(
([availableModelId, info]) => ({
modelId: availableModelId,
name: info.name ?? availableModelId,
description: info.description,
}),
);
return {
modes: {
availableModes: this.availableModes(),
currentModeId: session.currentMode,
},
models: {
availableModels,
currentModelId: session.currentModelId,
},
configOptions: await buildAllConfigOptions(session),
};
}
async prompt(params: PromptRequest): Promise<PromptResponse> {
const session = this.sessions.get(params.sessionId);
if (!session) {
@@ -193,6 +324,7 @@ export class AcpAgent implements Agent {
const abortController = new AbortController();
session.abortController = abortController;
session.fatalError = undefined;
// If cancel() was already called before prompt() started, bail early.
if (abortController.signal.aborted) {
@@ -242,6 +374,17 @@ export class AcpAgent implements Agent {
updatedAt: new Date().toISOString(),
});
// A cancelled turn always reports `cancelled`: the ACP spec
// requires agents to convert abort failures into the cancelled stop reason
// so clients don't show cancellations as errors.
if (stopReason !== "cancelled") {
const fatalError = session.fatalError;
session.fatalError = undefined;
if (fatalError) {
throw toAcpPromptError(fatalError);
}
}
return { stopReason };
}
@@ -326,16 +469,37 @@ export class AcpAgent implements Agent {
// creates a fresh one with the new provider on the next prompt().
await this.teardownSessionManager(session);
// If current model doesn't exist in new provider, reset to first available
// Re-resolve the model against the new provider's catalog: keep the
// current one when it's offered there too, otherwise fall back to the
// provider's declared default rather than whichever model happens to
// be listed first (for cline-pass that is an unrelated free model).
const providerModels = await Llms.getModelsForProvider(value);
const modelIds = Object.keys(providerModels);
const fallbackModelId = modelIds[0];
if (
!modelIds.includes(session.currentModelId) &&
fallbackModelId !== undefined
) {
session.currentModelId = fallbackModelId;
session.currentModelId = await resolveDefaultModelId(
value,
session.currentModelId,
providerModels,
);
break;
}
case ORGANIZATION_CONFIG_ID: {
try {
await switchClineOrganization({
apiKey: this.accountApiKey,
providerSettingsManager: this.providerSettingsManager,
organizationId: value === PERSONAL_ACCOUNT_VALUE ? null : value,
});
} catch (error) {
const message = describeAgentError(error);
throw RequestError.internalError(
{ message },
`Failed to switch account: ${message}`,
);
}
// Restart the backend session so subsequent turns run under the
// newly selected account.
await this.teardownSessionManager(session);
break;
}
@@ -362,6 +526,18 @@ export class AcpAgent implements Agent {
break;
}
case AUTO_APPROVE_CONFIG_ID: {
const autoApprove = parseAutoApproveValue(params.value);
if (autoApprove === undefined) {
throw RequestError.invalidParams(
undefined,
`Invalid auto-approve value: ${String(params.value)} (must be a boolean)`,
);
}
session.autoApproveTools = autoApprove;
break;
}
default:
throw RequestError.invalidParams(
undefined,
@@ -370,6 +546,12 @@ export class AcpAgent implements Agent {
}
const configOptions = await buildAllConfigOptions(session);
const organizationOption = await this.getOrganizationConfigOption(
session.currentProviderId,
);
if (organizationOption) {
configOptions.push(organizationOption);
}
sendConfigOptionUpdate(this.conn, params.sessionId, configOptions);
return { configOptions };
}
@@ -410,6 +592,25 @@ export class AcpAgent implements Agent {
this.sessions.clear();
}
private get accountApiKey(): string {
return process.env.CLINE_API_KEY ?? this.authResult?.apiKey ?? "";
}
private async getOrganizationConfigOption(
providerId: string,
): Promise<SessionConfigOption | undefined> {
if (!usesClineAccount(providerId)) {
return undefined;
}
const organizations = await fetchClineOrganizations({
apiKey: this.accountApiKey,
providerSettingsManager: this.providerSettingsManager,
});
return organizations
? buildOrganizationConfigOption(organizations)
: undefined;
}
/**
* Attempt to restore authentication from persisted provider settings.
*
@@ -467,13 +668,17 @@ export class AcpAgent implements Agent {
* Lazily create and start the session manager for this ACP session.
* After the first call the manager persists across prompt() calls so that
* conversation history is maintained.
*
* With `resume: true` the persisted conversation for `acpSessionId` is read
* back through the session manager.
*/
private async ensureSessionManager(
session: SessionState,
acpSessionId: string,
): Promise<void> {
options?: { resume?: boolean },
): Promise<Message[] | undefined> {
if (session.sessionManager) {
return;
return undefined;
}
const config = await this.buildConfig(session);
@@ -482,35 +687,66 @@ export class AcpAgent implements Agent {
toolPolicies: config.toolPolicies,
capabilities: {
requestToolApproval: (request) =>
requestAcpToolApproval(this.conn, acpSessionId, request),
session.autoApproveTools
? Promise.resolve({ approved: true })
: requestAcpToolApproval(this.conn, acpSessionId, request),
},
cwd: config.cwd,
workspaceRoot: config.workspaceRoot,
});
let initialMessages: Message[] | undefined;
if (options?.resume) {
initialMessages = await sessionManager
.readMessages(acpSessionId)
.catch(() => undefined);
if (!initialMessages || initialMessages.length === 0) {
await sessionManager
.dispose("acp_load_session_not_found")
.catch(() => {});
throw RequestError.resourceNotFound(acpSessionId);
}
} else {
initialMessages = session.pendingInitialMessages;
session.pendingInitialMessages = undefined;
}
session.unsubscribe = subscribeToAgentEvents(
sessionManager,
(event: AgentEvent) => {
// Remember unrecoverable failures so prompt() can fail the turn.
if (event.type === "error" && !event.recoverable) {
session.fatalError =
event.error instanceof Error
? event.error
: new Error(describeAgentError(event.error));
}
forwardAgentEvent(this.conn, acpSessionId, event);
},
);
const initialMessages = session.pendingInitialMessages;
session.pendingInitialMessages = undefined;
const started = await sessionManager.start({
source: SessionSource.CLI,
config,
// Persist the core session under the ACP session id so that
// session/load can find the conversation by the id the client holds.
config: {
...config,
modelId: session.currentModelId,
sessionId: acpSessionId,
},
interactive: true,
initialMessages,
});
session.sessionManager = sessionManager;
session.activeSessionId = started.sessionId;
return initialMessages;
}
private async buildConfig(session: SessionState): Promise<Config> {
const cwd = session.cwd || process.cwd();
const workspaceRoot = resolveWorkspaceRoot(cwd);
// Resolve credentials: env vars take precedence, then session provider.
const providerId = process.env.CLINE_PROVIDER ?? session.currentProviderId;
const apiKey = process.env.CLINE_API_KEY ?? this.authResult?.apiKey ?? "";
@@ -519,6 +755,7 @@ export class AcpAgent implements Agent {
providerId,
mode: session.currentMode,
});
const cliBuildInfo = getCliBuildInfo();
return {
providerId,
@@ -537,11 +774,69 @@ export class AcpAgent implements Agent {
enableAgentTeams: false,
enableTools: true,
cwd,
workspaceRoot: resolveWorkspaceRoot(cwd),
workspaceRoot,
extensionContext: {
client: {
name: "cline-acp",
version: cliBuildInfo.version,
platform: "cli",
platformVersion: cliBuildInfo.version,
isMultiRoot: false,
},
workspace: {
rootPath: workspaceRoot,
cwd,
workspaceName: cwd,
ide: "Terminal Shell",
platform: process.platform,
},
},
};
}
}
async function resolveDefaultModelId(
providerId: string,
preferredModelId: string | undefined,
providerModels: Record<string, unknown>,
): Promise<string> {
const modelIds = Object.keys(providerModels);
const preferred = preferredModelId?.trim();
if (preferred && modelIds.includes(preferred)) {
return preferred;
}
const providerDefault = (await Llms.getProvider(providerId))?.defaultModelId;
if (providerDefault && modelIds.includes(providerDefault)) {
return providerDefault;
}
return modelIds[0] ?? "";
}
/**
* Convert a fatal agent error into a JSON-RPC error for the prompt response.
*
* Credential/subscription problems map to `auth_required` (-32000) so clients
* can offer a re-auth affordance rather than just printing text; everything
* else is an internal error.
*
* Classification goes through the shared CLI helpers, which check the error's
* type *and* its name/message. That matters because the runtime re-wraps errors
* as it forwards them across the event boundary, so `instanceof` alone fails on
* the object ACP actually receives.
*/
function toAcpPromptError(error: Error): RequestError {
if (isClineOrgIndividualInferenceSubscriptionErrorMessage(error)) {
const message = getAcpOrgSubscriptionMessage();
return RequestError.internalError({ message }, message);
}
const message = describeAgentError(error);
const isAuthProblem = isLikelyAuthError(error);
return isAuthProblem
? RequestError.authRequired({ message }, message)
: RequestError.internalError({ message }, message);
}
async function buildProviderConfigOption(
currentProviderId: string,
): Promise<SessionConfigOption> {
@@ -618,6 +913,7 @@ async function buildAllConfigOptions(
providerOption,
buildModelConfigOption(session.currentModelId, providerModels),
buildModeConfigOption(session.currentMode),
buildAutoApproveConfigOption(session.autoApproveTools),
];
}
+5 -1
View File
@@ -5,9 +5,13 @@ import { writeDiagnostic } from "../utils/output";
/**
* Supported ACP OAuth provider IDs.
*
* This list doubles as the set of selectable providers (see
* `setSessionConfigOption`)
*/
export const ACP_AUTH_METHODS = [
{ id: "cline", name: "Sign in with Cline" },
{ id: "cline-pass", name: "Sign in with ClinePass" },
{ id: "openai-codex", name: "Sign in with ChatGPT Subscription" },
] as const;
@@ -30,7 +34,7 @@ async function performOAuthLogin(input: {
providerSettingsManager: ProviderSettingsManager;
}): Promise<string> {
const [{ createOAuthClientCallbacks }, { default: open }] = await Promise.all(
[import("@cline/core"), import("open")],
[import("@cline/core"), import("../utils/open")],
);
const callbacks = createOAuthClientCallbacks({
+47
View File
@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import {
AUTO_APPROVE_CONFIG_ID,
buildAutoApproveConfigOption,
parseAutoApproveValue,
} from "./auto-approve";
describe("buildAutoApproveConfigOption", () => {
it("builds a boolean config option reflecting the current value", () => {
const option = buildAutoApproveConfigOption(true);
expect(option).toMatchObject({
type: "boolean",
id: AUTO_APPROVE_CONFIG_ID,
currentValue: true,
});
expect(option.name).toBeTruthy();
});
it("defaults to disabled when the session has it off", () => {
const option = buildAutoApproveConfigOption(false);
expect(option).toMatchObject({ type: "boolean", currentValue: false });
});
});
describe("parseAutoApproveValue", () => {
it("accepts booleans", () => {
expect(parseAutoApproveValue(true)).toBe(true);
expect(parseAutoApproveValue(false)).toBe(false);
});
it("accepts the string forms sent by older clients", () => {
expect(parseAutoApproveValue("true")).toBe(true);
expect(parseAutoApproveValue("false")).toBe(false);
});
it("fails closed for unrecognized values", () => {
expect(parseAutoApproveValue("yes")).toBeUndefined();
expect(parseAutoApproveValue(1)).toBeUndefined();
expect(parseAutoApproveValue(null)).toBeUndefined();
});
it("returns undefined when no value was provided", () => {
expect(parseAutoApproveValue(undefined)).toBeUndefined();
});
});
+32
View File
@@ -0,0 +1,32 @@
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
export const AUTO_APPROVE_CONFIG_ID = "auto_approve";
export function buildAutoApproveConfigOption(
currentValue: boolean,
): SessionConfigOption {
return {
type: "boolean",
id: AUTO_APPROVE_CONFIG_ID,
name: "Auto-approve tools",
description:
"Automatically approve all tool calls without asking for permission",
currentValue,
};
}
/**
* Interpret the value of a `session/set_config_option` request for the
* auto-approve option.
*
* The ACP schema sends booleans for boolean options, but clients that predate
* boolean options may send the string form, so both are accepted. Returns
* `undefined` for anything else so the caller can reject the request.
*/
export function parseAutoApproveValue(value: unknown): boolean | undefined {
if (typeof value === "boolean" || value === undefined) {
return value;
}
return value === "true" ? true : value === "false" ? false : undefined;
}
+8 -2
View File
@@ -1,7 +1,11 @@
import { Readable, Writable } from "node:stream";
import { writeDiagnostic } from "../utils/output";
export async function runAcpMode(): Promise<void> {
export interface AcpModeOptions {
autoApproveTools?: boolean;
}
export async function runAcpMode(options?: AcpModeOptions): Promise<void> {
const { AgentSideConnection, ndJsonStream } = await import(
"@agentclientprotocol/sdk"
);
@@ -15,7 +19,9 @@ export async function runAcpMode(): Promise<void> {
);
const connection = new AgentSideConnection((conn) => {
return new AcpAgent(conn);
return new AcpAgent(conn, {
autoApproveTools: options?.autoApproveTools,
});
}, stream);
// Keep the process alive until the connection closes
+49
View File
@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import {
buildOrganizationConfigOption,
PERSONAL_ACCOUNT_VALUE,
} from "./organizations";
describe("buildOrganizationConfigOption", () => {
const organizations = [
{
active: false,
memberId: "m-1",
name: "Acme Corp",
organizationId: "org-1",
roles: ["member" as const],
},
{
active: true,
memberId: "m-2",
name: "Cline Bot Inc",
organizationId: "org-2",
roles: ["admin" as const],
},
];
it("lists Personal first plus every organization", () => {
const option = buildOrganizationConfigOption({
organizations,
activeOrganizationId: "org-2",
});
expect(option.id).toBe("organization");
if (option.type !== "select") {
throw new Error(`expected a select option, got ${option.type}`);
}
expect(option.currentValue).toBe("org-2");
expect(option.options).toEqual([
{ value: PERSONAL_ACCOUNT_VALUE, name: "Personal" },
{ value: "org-1", name: "Acme Corp" },
{ value: "org-2", name: "Cline Bot Inc" },
]);
});
it("selects Personal when no organization is active", () => {
const option = buildOrganizationConfigOption({
organizations,
activeOrganizationId: null,
});
expect(option.currentValue).toBe(PERSONAL_ACCOUNT_VALUE);
});
});
+154
View File
@@ -0,0 +1,154 @@
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
import {
type ClineAccountOrganization,
ClineAccountService,
getPersistedProviderApiKey,
type ProviderSettingsManager,
RuntimeOAuthTokenManager,
} from "@cline/core";
import { getClineEnvironmentConfig } from "@cline/shared";
export const PERSONAL_ACCOUNT_VALUE = "personal";
export const ORGANIZATION_CONFIG_ID = "organization";
export function usesClineAccount(providerId: string): boolean {
return providerId === "cline" || providerId === "cline-pass";
}
export interface AcpOrganizationState {
organizations: ClineAccountOrganization[];
/** Active organization id, or null when the personal account is active. */
activeOrganizationId: string | null;
}
interface ClineAccountInput {
apiKey: string;
providerSettingsManager: ProviderSettingsManager;
}
// Cline access tokens expire between runs, so account requests resolve
// through the refresh-aware OAuth manager. A single shared instance keeps
// refreshes single-flight; the refresh token is single-use, so parallel
// refreshes would invalidate each other.
let oauthTokenManager: RuntimeOAuthTokenManager | undefined;
function createAccountService(input: ClineAccountInput): ClineAccountService {
const { providerSettingsManager } = input;
const settings = providerSettingsManager.getProviderSettings("cline");
return new ClineAccountService({
apiBaseUrl:
settings?.baseUrl?.trim() || getClineEnvironmentConfig().apiBaseUrl,
getAuthToken: async () => {
try {
oauthTokenManager ??= new RuntimeOAuthTokenManager({
providerSettingsManager,
});
const resolution = await oauthTokenManager.resolveProviderApiKey({
providerId: "cline",
});
if (resolution?.apiKey) {
return resolution.apiKey;
}
} catch {
// Fall back to the persisted token; the account request surfaces
// the auth failure to the caller.
}
return (
getPersistedProviderApiKey(
"cline",
providerSettingsManager.getProviderSettings("cline"),
) ||
input.apiKey ||
undefined
);
},
});
}
export async function fetchClineOrganizations(
input: ClineAccountInput,
): Promise<AcpOrganizationState | undefined> {
try {
const service = createAccountService(input);
const organizations = await service.fetchUserOrganizations();
if (organizations.length === 0) {
return undefined;
}
return {
organizations,
activeOrganizationId:
organizations.find((org) => org.active)?.organizationId ?? null,
};
} catch {
return undefined;
}
}
export function buildOrganizationConfigOption(
state: AcpOrganizationState,
): SessionConfigOption {
return {
type: "select",
id: ORGANIZATION_CONFIG_ID,
name: "Account",
description:
"The Cline account usage is billed to — your personal account or an organization",
category: "account",
currentValue: state.activeOrganizationId ?? PERSONAL_ACCOUNT_VALUE,
options: [
{ value: PERSONAL_ACCOUNT_VALUE, name: "Personal" },
...state.organizations.map((org) => ({
value: org.organizationId,
name: org.name,
})),
],
};
}
export async function switchClineOrganization(
input: ClineAccountInput & { organizationId: string | null },
): Promise<void> {
const service = createAccountService(input);
await service.switchAccount(input.organizationId);
await persistActiveOrganization(input.providerSettingsManager, service);
}
// Re-persist the active organization so headless runs and the hub daemon
// attribute telemetry to the right account. Best-effort: the switch itself
// already succeeded server-side.
async function persistActiveOrganization(
manager: ProviderSettingsManager,
service: ClineAccountService,
): Promise<void> {
try {
const organizations = await service.fetchUserOrganizations();
const active = organizations.find((org) => org.active) ?? null;
const persisted = manager.getProviderSettings("cline");
if (!persisted) {
return;
}
manager.saveProviderSettings(
{
...persisted,
auth: {
...persisted.auth,
organizationId: active?.organizationId,
organizationName: active?.name,
memberId: active?.memberId,
},
},
{ setLastUsed: false },
);
} catch {
// Ignore; see above.
}
}
export function getAcpOrgSubscriptionMessage(): string {
return [
"Organization accounts cannot use ClinePass subscriptions.",
'Switch the "Account" session option to Personal to keep using ClinePass,',
'or switch the "Provider" option to Cline to bill your organization.',
].join(" ");
}
+261
View File
@@ -0,0 +1,261 @@
import type { AgentSideConnection } from "@agentclientprotocol/sdk";
import { describe, expect, it, vi } from "vitest";
import { ACT_MODE_CONTINUATION_PROMPT } from "../runtime/interactive/mode";
import {
replaySessionHistory,
translateHistoricalMessage,
} from "./session-load";
describe("translateHistoricalMessage", () => {
it("maps string content to a message chunk for the right role", () => {
expect(translateHistoricalMessage({ role: "user", content: "hi" })).toEqual(
[
{
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "hi" },
},
],
);
expect(
translateHistoricalMessage({ role: "assistant", content: "hello" }),
).toEqual([
{
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "hello" },
},
]);
});
it("strips the <user_input> wrapper from replayed user text", () => {
// Persisted user messages keep their runtime-generated wrapper. Replaying
// it verbatim leaked markup to the client, which rendered the unknown
// element as bare text (a one-word prompt showed up as just its content
// with the wrapper swallowed).
expect(
translateHistoricalMessage({
role: "user",
content: '<user_input mode="act">s</user_input>',
}),
).toEqual([
{
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "s" },
},
]);
expect(
translateHistoricalMessage({
role: "user",
content: [
{
type: "text",
text: '<user_input mode="plan">lets do it</user_input>',
},
],
}),
).toEqual([
{
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "lets do it" },
},
]);
});
it("strips mode notices and formats slash commands for display", () => {
expect(
translateHistoricalMessage({
role: "user",
content:
'<user_input mode="plan"><mode_notice>The user switched from act mode to plan mode before sending this message.</mode_notice>\nare you okay?</user_input>',
}),
).toEqual([
{
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "are you okay?" },
},
]);
expect(
translateHistoricalMessage({
role: "user",
content:
'<user_command slash="team">spawn a team of agents for the following task: inspect rpc startup</user_command>',
}),
).toEqual([
{
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "/team inspect rpc startup" },
},
]);
});
it("does not replay the synthetic act-mode continuation prompt", () => {
expect(
translateHistoricalMessage({
role: "user",
content: `<user_input mode="act">${ACT_MODE_CONTINUATION_PROMPT}</user_input>`,
}),
).toEqual([]);
});
it("leaves assistant text untouched", () => {
// Only user text carries the wrapper; agent output must replay verbatim.
expect(
translateHistoricalMessage({
role: "assistant",
content: 'Use <user_input mode="act"> to wrap prompts.',
}),
).toEqual([
{
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: 'Use <user_input mode="act"> to wrap prompts.',
},
},
]);
});
it("skips empty text and unknown blocks", () => {
expect(
translateHistoricalMessage({
role: "assistant",
content: [
{ type: "text", text: "" },
{ type: "redacted_thinking", data: "xxx" },
],
}),
).toEqual([]);
});
it("maps thinking blocks to agent_thought_chunk", () => {
expect(
translateHistoricalMessage({
role: "assistant",
content: [{ type: "thinking", thinking: "pondering" }],
}),
).toEqual([
{
sessionUpdate: "agent_thought_chunk",
content: { type: "text", text: "pondering" },
},
]);
});
it("maps tool_use to a pending tool_call", () => {
const updates = translateHistoricalMessage({
role: "assistant",
content: [
{
type: "tool_use",
id: "call-1",
name: "read_files",
input: { file_paths: ["a.ts"] },
},
],
});
expect(updates).toHaveLength(1);
expect(updates[0]).toMatchObject({
sessionUpdate: "tool_call",
toolCallId: "call-1",
kind: "read",
status: "pending",
rawInput: { file_paths: ["a.ts"] },
});
});
it("maps tool_result to a tool_call_update with flattened output", () => {
expect(
translateHistoricalMessage({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "call-1",
name: "read_files",
content: [
{ type: "text", text: "line one" },
{ type: "image", data: "abc", mediaType: "image/png" },
],
},
],
}),
).toEqual([
{
sessionUpdate: "tool_call_update",
toolCallId: "call-1",
status: "completed",
rawOutput: "line one\n[image]",
},
]);
});
it("marks errored tool results as failed", () => {
const [update] = translateHistoricalMessage({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "call-2",
name: "run_commands",
content: "boom",
is_error: true,
},
],
});
expect(update).toMatchObject({
sessionUpdate: "tool_call_update",
toolCallId: "call-2",
status: "failed",
rawOutput: "boom",
});
});
it("maps image blocks to image content chunks", () => {
expect(
translateHistoricalMessage({
role: "user",
content: [{ type: "image", data: "abc", mediaType: "image/png" }],
}),
).toEqual([
{
sessionUpdate: "user_message_chunk",
content: { type: "image", data: "abc", mimeType: "image/png" },
},
]);
});
});
describe("replaySessionHistory", () => {
it("sends one awaited notification per update, in order", async () => {
const sent: unknown[] = [];
const conn = {
sessionUpdate: vi.fn(async (notification: unknown) => {
sent.push(notification);
}),
} as unknown as AgentSideConnection;
await replaySessionHistory(conn, "sess-1", [
{ role: "user", content: "question" },
{ role: "assistant", content: "answer" },
]);
expect(sent).toEqual([
{
sessionId: "sess-1",
update: {
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "question" },
},
},
{
sessionId: "sess-1",
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "answer" },
},
},
]);
});
});
+141
View File
@@ -0,0 +1,141 @@
import type {
AgentSideConnection,
SessionUpdate,
} from "@agentclientprotocol/sdk";
import {
type ContentBlock,
formatDisplayUserInput,
type Message,
type ToolResultContent,
} from "@cline/shared";
import { ACT_MODE_CONTINUATION_PROMPT } from "../runtime/interactive/mode";
import { buildToolTitle, mapToolKind } from "./tool-utils";
/**
* The act-mode continuation prompt is runtime-generated, not typed by the
* user, so it must not replay as a user turn. Mirrors the TUI transcript
* hydration filter in tui/utils/hydrate-messages.ts.
*/
function isSyntheticUserText(text: string): boolean {
return text === ACT_MODE_CONTINUATION_PROMPT;
}
/**
* Replay a persisted conversation to the client as session/update
* notifications. Used by `session/load` — the ACP spec requires the entire
* conversation to be replayed before the load request resolves, so each
* notification is awaited.
*/
export async function replaySessionHistory(
conn: AgentSideConnection,
sessionId: string,
messages: Message[],
): Promise<void> {
for (const message of messages) {
for (const update of translateHistoricalMessage(message)) {
await conn.sessionUpdate({ sessionId, update });
}
}
}
export function translateHistoricalMessage(message: Message): SessionUpdate[] {
const blocks: ContentBlock[] =
typeof message.content === "string"
? [{ type: "text", text: message.content }]
: message.content;
const updates: SessionUpdate[] = [];
for (const block of blocks) {
switch (block.type) {
case "text": {
if (!block.text) break;
if (message.role !== "user") {
updates.push({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: block.text },
});
break;
}
// Display boundary: persisted user text keeps its runtime-generated
// <user_input mode="..."> wrapper and <mode_notice> elements (they are
// the durable record of the mode each turn was sent in). Replaying them
// verbatim leaks markup to the client, which renders the unknown
// element as bare text — so `s` shows up as `s` with the wrapper
// swallowed. Strip them the same way every other surface does.
const text = formatDisplayUserInput(block.text);
if (!text || isSyntheticUserText(text)) break;
updates.push({
sessionUpdate: "user_message_chunk",
content: { type: "text", text },
});
break;
}
case "thinking": {
if (!block.thinking) break;
updates.push({
sessionUpdate: "agent_thought_chunk",
content: { type: "text", text: block.thinking },
});
break;
}
case "image": {
const content = {
type: "image" as const,
data: block.data,
mimeType: block.mediaType,
};
updates.push(
message.role === "user"
? { sessionUpdate: "user_message_chunk", content }
: { sessionUpdate: "agent_message_chunk", content },
);
break;
}
case "tool_use": {
updates.push({
sessionUpdate: "tool_call",
toolCallId: block.id,
title: buildToolTitle(block.name, block.input),
kind: mapToolKind(block.name),
status: "pending",
rawInput: block.input,
});
break;
}
case "tool_result": {
updates.push({
sessionUpdate: "tool_call_update",
toolCallId: block.tool_use_id,
status: block.is_error ? "failed" : "completed",
rawOutput: flattenToolResultContent(block.content),
});
break;
}
default:
break;
}
}
return updates;
}
function flattenToolResultContent(
content: ToolResultContent["content"],
): string {
if (typeof content === "string") {
return content;
}
return content
.map((part) => {
switch (part.type) {
case "text":
return part.text;
case "file":
return part.content;
default:
return "[image]";
}
})
.join("\n");
}
+6
View File
@@ -4,6 +4,7 @@ import type {
SessionUpdate,
} from "@agentclientprotocol/sdk";
import type { AgentEvent } from "@cline/core";
import { getErrorMessage } from "@cline/shared";
import { buildToolTitle, mapToolKind } from "./tool-utils";
/**
@@ -81,6 +82,11 @@ function translateContentStart(
}
}
export function describeAgentError(error: unknown): string {
const message = getErrorMessage(error).trim();
return message || "The agent reported an unknown error.";
}
function translateContentEnd(
event: AgentEvent & { type: "content_end" },
): SessionUpdate[] {
+367
View File
@@ -0,0 +1,367 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
// The helper ships as CommonJS in the published wrapper package, so it is
// loaded via require rather than an ESM import.
const caCerts = require("../../bin/ca-certs.cjs") as {
harvestSystemCerts: (tls?: unknown) => string[];
readUserBundle: (fs: unknown, p: string | null) => string | null;
readUserCerts: (
fs: unknown,
path: unknown,
value: string | null,
managedPath: string | null,
) => string[];
buildBundle: (input: {
systemCerts: string[];
userPems?: string[];
}) => string;
countCerts: (pems: string[]) => number;
configureNodeExtraCaCerts: (
env: Record<string, string>,
deps?: { tls?: unknown; fs?: unknown },
) => {
action: string;
path: string | null;
systemCertCount: number;
userCertCount: number;
};
shouldWarnApiUnavailable: (
env: Record<string, string>,
deps?: { fs?: unknown; nodeVersion?: string },
) => boolean;
};
const fs = require("node:fs");
const path = require("node:path");
const certSystem =
"-----BEGIN CERTIFICATE-----\nSYSTEM\n-----END CERTIFICATE-----\n";
const certUser = "-----BEGIN CERTIFICATE-----\nUSER\n-----END CERTIFICATE-----";
function fakeTls(certs: unknown) {
return { getCACertificates: () => certs };
}
describe("ca-certs", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "cline-ca-"));
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
describe("harvestSystemCerts", () => {
it("returns only PEM strings from the system store", () => {
expect(
caCerts.harvestSystemCerts(fakeTls([certSystem, "not-a-cert", 42])),
).toEqual([certSystem]);
});
it("returns [] when getCACertificates is unavailable", () => {
expect(caCerts.harvestSystemCerts({})).toEqual([]);
});
it("returns [] when getCACertificates throws", () => {
expect(
caCerts.harvestSystemCerts({
getCACertificates: () => {
throw new Error("nope");
},
}),
).toEqual([]);
});
});
describe("readUserBundle", () => {
it("returns PEM contents for a PEM file", () => {
const p = join(dir, "user.pem");
writeFileSync(p, certUser);
expect(caCerts.readUserBundle(fs, p)).toBe(certUser);
});
it("returns null for a non-PEM (DER) file", () => {
const p = join(dir, "user.der");
writeFileSync(p, Buffer.from([0x30, 0x82, 0x01, 0x02]));
expect(caCerts.readUserBundle(fs, p)).toBeNull();
});
it("returns null for a missing file and for null path", () => {
expect(caCerts.readUserBundle(fs, join(dir, "nope.pem"))).toBeNull();
expect(caCerts.readUserBundle(fs, null)).toBeNull();
});
it("strips non-certificate sections such as private keys", () => {
// Combined cert+key files (nginx/haproxy style) are common; the key
// must never reach the managed bundle.
const p = join(dir, "combined.pem");
writeFileSync(
p,
`${certUser}\n-----BEGIN PRIVATE KEY-----\nSECRET\n-----END PRIVATE KEY-----\n`,
);
const out = caCerts.readUserBundle(fs, p);
expect(out).toContain("USER");
expect(out).not.toContain("PRIVATE KEY");
expect(out).not.toContain("SECRET");
});
it("keeps certificates-only files verbatim", () => {
// Byte-identical passthrough keeps the unchanged-skip hash stable.
const p = join(dir, "clean.pem");
writeFileSync(p, `${certUser}\n${certSystem}`);
expect(caCerts.readUserBundle(fs, p)).toBe(`${certUser}\n${certSystem}`);
});
it("returns null for a BEGIN marker without a complete block", () => {
const p = join(dir, "truncated.pem");
writeFileSync(p, "-----BEGIN CERTIFICATE-----\ntruncated");
expect(caCerts.readUserBundle(fs, p)).toBeNull();
});
});
describe("readUserCerts", () => {
it("reads a single PEM file path", () => {
const p = join(dir, "corp.pem");
writeFileSync(p, certUser);
expect(caCerts.readUserCerts(fs, path, p, null)).toEqual([certUser]);
});
it("splits a legacy OS-path-delimited value and reads each PEM", () => {
// Legacy footgun: NODE_EXTRA_CA_CERTS="a.pem;b.pem".
const a = join(dir, "a.pem");
const b = join(dir, "b.pem");
writeFileSync(a, certUser);
writeFileSync(b, certSystem);
expect(
caCerts.readUserCerts(fs, path, [a, b].join(delimiter), null),
).toEqual([certUser, certSystem]);
});
it("skips missing segments in a delimited value", () => {
const a = join(dir, "a.pem");
writeFileSync(a, certUser);
const value = [a, join(dir, "missing.pem")].join(delimiter);
expect(caCerts.readUserCerts(fs, path, value, null)).toEqual([certUser]);
});
it("excludes the managed bundle from user certs", () => {
const managed = join(dir, "cli-node-extra-ca-certs.pem");
writeFileSync(managed, certUser);
expect(caCerts.readUserCerts(fs, path, managed, managed)).toEqual([]);
});
it("returns [] for empty value", () => {
expect(caCerts.readUserCerts(fs, path, null, null)).toEqual([]);
});
});
describe("buildBundle", () => {
it("merges user PEMs before system certs", () => {
expect(
caCerts.buildBundle({
systemCerts: [certSystem],
userPems: [certUser],
}),
).toBe(`${certUser}\n${certSystem}`);
});
it("inserts a separating newline so END/BEGIN markers do not fuse", () => {
// certUser has no trailing newline, so this proves the boundary fix.
const merged = caCerts.buildBundle({
systemCerts: [certSystem],
userPems: [certUser],
});
expect(merged).not.toContain(
"-----END CERTIFICATE----------BEGIN CERTIFICATE-----",
);
});
it("handles no user PEMs", () => {
expect(caCerts.buildBundle({ systemCerts: [certSystem] })).toBe(
certSystem,
);
});
});
describe("configureNodeExtraCaCerts", () => {
it("writes a managed bundle and points the env var at it", () => {
const env: Record<string, string> = { CLINE_DIR: dir };
const out = caCerts.configureNodeExtraCaCerts(env, {
tls: fakeTls([certSystem]),
});
expect(out.action).toBe("written");
expect(out.path).toBe(join(dir, "cli-node-extra-ca-certs.pem"));
expect(env.NODE_EXTRA_CA_CERTS).toBe(out.path);
expect(readFileSync(out.path as string, "utf8")).toContain("SYSTEM");
});
it("merges a user-supplied NODE_EXTRA_CA_CERTS with system certs", () => {
const userPath = join(dir, "corp.pem");
writeFileSync(userPath, certUser);
const env: Record<string, string> = {
CLINE_DIR: dir,
NODE_EXTRA_CA_CERTS: userPath,
};
const out = caCerts.configureNodeExtraCaCerts(env, {
tls: fakeTls([certSystem]),
});
expect(out.userCertCount).toBe(1);
const written = readFileSync(env.NODE_EXTRA_CA_CERTS, "utf8");
expect(written).toContain("USER");
expect(written).toContain("SYSTEM");
});
it("reports unchanged and skips rewrite on the second run", () => {
const env: Record<string, string> = { CLINE_DIR: dir };
expect(
caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([certSystem]) })
.action,
).toBe("written");
expect(
caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([certSystem]) })
.action,
).toBe("unchanged");
});
it("does not re-append when the user already points at the managed bundle", () => {
const env: Record<string, string> = { CLINE_DIR: dir };
const first = caCerts.configureNodeExtraCaCerts(env, {
tls: fakeTls([certSystem]),
}).path as string;
const env2: Record<string, string> = {
CLINE_DIR: dir,
NODE_EXTRA_CA_CERTS: first,
};
caCerts.configureNodeExtraCaCerts(env2, { tls: fakeTls([certSystem]) });
const written = readFileSync(env2.NODE_EXTRA_CA_CERTS, "utf8");
expect(written.match(/SYSTEM/g)?.length).toBe(1);
});
it("no-ops when no system certs are available", () => {
const env: Record<string, string> = {
CLINE_DIR: dir,
NODE_EXTRA_CA_CERTS: "/user/corp.pem",
};
const out = caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([]) });
expect(out.action).toBe("no-system-certs");
expect(out.path).toBeNull();
expect(env.NODE_EXTRA_CA_CERTS).toBe("/user/corp.pem");
});
it("reports api-unavailable on Nodes without getCACertificates", () => {
const env: Record<string, string> = {
CLINE_DIR: dir,
NODE_EXTRA_CA_CERTS: "/user/corp.pem",
};
const out = caCerts.configureNodeExtraCaCerts(env, { tls: {} });
expect(out.action).toBe("api-unavailable");
expect(out.path).toBeNull();
expect(env.NODE_EXTRA_CA_CERTS).toBe("/user/corp.pem");
});
it("reports write-failed when the bundle cannot be written", () => {
const realFs = require("node:fs");
const failingFs = {
...realFs,
mkdirSync: () => {
throw new Error("EACCES");
},
writeFileSync: () => {
throw new Error("EACCES");
},
};
const env: Record<string, string> = { CLINE_DIR: dir };
const out = caCerts.configureNodeExtraCaCerts(env, {
tls: fakeTls([certSystem]),
fs: failingFs,
});
expect(out.action).toBe("write-failed");
expect(out.path).toBeNull();
expect(env.NODE_EXTRA_CA_CERTS).toBeUndefined();
});
it("reuses a stale bundle when the rewrite fails", () => {
// First run writes the bundle normally.
const env: Record<string, string> = { CLINE_DIR: dir };
const managedPath = caCerts.configureNodeExtraCaCerts(env, {
tls: fakeTls([certSystem]),
}).path as string;
// Second run: writes fail, but the stale bundle is still readable.
const realFs = require("node:fs");
const failingFs = {
...realFs,
mkdirSync: () => {
throw new Error("EACCES");
},
writeFileSync: () => {
throw new Error("EACCES");
},
};
const env2: Record<string, string> = { CLINE_DIR: dir };
const out = caCerts.configureNodeExtraCaCerts(env2, {
// A different system cert forces a rewrite attempt (not "unchanged").
tls: fakeTls([certUser]),
fs: failingFs,
});
expect(out.action).toBe("write-failed-reused");
expect(env2.NODE_EXTRA_CA_CERTS).toBe(managedPath);
});
});
describe("countCerts", () => {
it("counts individual certificates, not files", () => {
// One file holding two certs must report 2, not 1.
const twoInOne = `${certUser}\n${certSystem}`;
expect(caCerts.countCerts([twoInOne])).toBe(2);
expect(caCerts.countCerts([certUser, certSystem])).toBe(2);
expect(caCerts.countCerts([])).toBe(0);
});
});
describe("shouldWarnApiUnavailable", () => {
it("warns once per Node version, then stays quiet", () => {
const env = { CLINE_DIR: dir };
const deps = { nodeVersion: "22.1.0" };
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(false);
});
it("re-arms when the Node version changes", () => {
const env = { CLINE_DIR: dir };
expect(
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.1.0" }),
).toBe(true);
expect(
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.14.0" }),
).toBe(true);
expect(
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.1.0" }),
).toBe(false);
});
it("still warns when the stamp cannot be written", () => {
const realFs = require("node:fs");
const failingFs = {
...realFs,
mkdirSync: () => {
throw new Error("EACCES");
},
writeFileSync: () => {
throw new Error("EACCES");
},
};
const env = { CLINE_DIR: dir };
const deps = { fs: failingFs, nodeVersion: "22.1.0" };
// Bookkeeping failure must never suppress the diagnostic.
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
});
});
});
+65
View File
@@ -767,6 +767,71 @@ Break work into clear steps.`,
);
});
it("routes mcp uninstall and its rm alias", () => {
const tempRoot = mkdtempSync(path.join(os.tmpdir(), "cli-e2e-mcp-rm-"));
tempDirs.push(tempRoot);
const settingsPath = path.join(tempRoot, "cline_mcp_settings.json");
const writeSettings = () => {
writeFileSync(
settingsPath,
JSON.stringify(
{
mcpServers: {
docs: { transport: { type: "stdio", command: "node" } },
remote: {
transport: {
type: "streamableHttp",
url: "https://mcp.example.com",
},
},
},
customTopLevelKey: true,
},
null,
2,
),
"utf8",
);
};
const readServers = () =>
(
JSON.parse(readFileSync(settingsPath, "utf8")) as {
mcpServers?: Record<string, unknown>;
}
).mcpServers ?? {};
writeSettings();
const uninstallResult = runCli(["mcp", "uninstall", "docs"], {
env: { ...createIsolatedEnv(), CLINE_MCP_SETTINGS_PATH: settingsPath },
});
expect(uninstallResult.status).toBe(0);
expect(asText(uninstallResult.stdout)).toContain(
"Uninstalled MCP server docs.",
);
expect(Object.keys(readServers())).toEqual(["remote"]);
writeSettings();
const aliasResult = runCli(["mcp", "rm", "remote", "--json"], {
env: { ...createIsolatedEnv(), CLINE_MCP_SETTINGS_PATH: settingsPath },
});
expect(aliasResult.status).toBe(0);
expect(JSON.parse(asText(aliasResult.stdout).trim())).toEqual({
name: "remote",
status: "uninstalled",
});
expect(Object.keys(readServers())).toEqual(["docs"]);
writeSettings();
const missingResult = runCli(["mcp", "remove", "missing"], {
env: { ...createIsolatedEnv(), CLINE_MCP_SETTINGS_PATH: settingsPath },
});
expect(missingResult.status).toBe(1);
expect(asText(missingResult.stderr)).toContain(
'MCP server "missing" is not installed.',
);
expect(Object.keys(readServers())).toEqual(["docs", "remote"]);
});
it("lists available tools", () => {
const homeDir = mkdtempSync(path.join(os.tmpdir(), "cli-e2e-home-"));
const dataDir = mkdtempSync(path.join(os.tmpdir(), "cli-e2e-data-"));
+1 -1
View File
@@ -220,7 +220,7 @@ describe("cli interactive e2e", () => {
"claude-sonnet-4-6",
"-k",
"test-key",
"hello",
"seed history session",
],
{ cwd: cliRoot, encoding: "utf8", env, timeout: 60_000 },
);
+244
View File
@@ -0,0 +1,244 @@
// ---------------------------------------------------------------------------
// Proof-of-concept: driving the interactive TUI with tuistory
// (https://github.com/remorses/tuistory) instead of `script` + timed printf.
//
// Compare with `cli.interactive.e2e.test.ts`, which pipes keystrokes through
// the Unix `script` utility on a fixed sleep schedule and greps the raw
// output dump. Here each test launches the CLI in a real PTY backed by a
// Ghostty terminal emulator, waits reactively for screen content
// (`waitForText` resolves as soon as the text renders), and asserts against
// the emulated screen state rather than the raw byte stream.
//
// Run with: bun run test:e2e:tuistory
// ---------------------------------------------------------------------------
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { launchTerminal, type Session } from "tuistory";
import { afterEach, describe, expect, it } from "vitest";
const cliRoot = path.resolve(__dirname, "..");
const cliEntry = path.join(cliRoot, "src", "index.ts");
const bunExec = process.env.BUN_EXEC_PATH ?? "bun";
const LAUNCH_TIMEOUT_MS = 30_000;
const UI_TIMEOUT_MS = 15_000;
const tempDirs: string[] = [];
const sessions: Session[] = [];
function createCliEnv(
overrides: Record<string, string | undefined> = {},
): Record<string, string | undefined> {
const homeDir = mkdtempSync(path.join(os.tmpdir(), "cli-tuistory-home-"));
const dataDir = mkdtempSync(path.join(os.tmpdir(), "cli-tuistory-data-"));
const sessionDir = mkdtempSync(
path.join(os.tmpdir(), "cli-tuistory-sessions-"),
);
const teamDir = mkdtempSync(path.join(os.tmpdir(), "cli-tuistory-teams-"));
tempDirs.push(homeDir, dataDir, sessionDir, teamDir);
return {
HOME: homeDir,
CLINE_DATA_DIR: dataDir,
CLINE_DB_DATA_DIR: path.join(dataDir, "db"),
CLINE_SESSION_DATA_DIR: sessionDir,
CLINE_TEAM_DATA_DIR: teamDir,
CLINE_SESSION_BACKEND_MODE: "local",
CLINE_PROVIDER_SETTINGS_PATH: path.join(
dataDir,
"settings",
"providers.json",
),
CLINE_HOOKS_LOG_PATH: path.join(dataDir, "logs", "hooks.jsonl"),
CLINE_TELEMETRY_DISABLED: "1",
CLINE_NO_AUTO_UPDATE: "1",
// Without this, the ClinePass promo dialog renders over the chat view.
// The stream-grepping interactive suite doesn't notice the overlay, but
// tuistory's screen snapshot reflects what the user actually sees.
CLINE_DISABLE_CLINE_PASS_NOTICE: "1",
// The parent vitest process sets CI/VITEST; clear them so the spawned
// CLI renders as a real interactive terminal.
CI: undefined,
VITEST: undefined,
...overrides,
};
}
async function launchCli(
extraArgs: string[] = [],
env: Record<string, string | undefined> = createCliEnv(),
): Promise<Session> {
const session = await launchTerminal({
command: bunExec,
args: [
cliEntry,
"--provider",
"anthropic",
"-m",
"claude-sonnet-4-6",
"-k",
"test-key",
...extraArgs,
],
cwd: cliRoot,
env,
cols: 120,
rows: 36,
// The CLI compiles a large TS graph on cold start; don't gate launch
// on the default 5s first-data timeout.
waitForDataTimeout: LAUNCH_TIMEOUT_MS,
});
sessions.push(session);
return session;
}
/** Wait for the chat view to be fully rendered. */
async function waitForChatView(session: Session): Promise<void> {
await session.waitForText("What can I do for you?", {
timeout: LAUNCH_TIMEOUT_MS,
});
}
describe("cli tuistory e2e", () => {
afterEach(async () => {
for (const session of sessions.splice(0)) {
try {
// Double Ctrl+C exits the TUI cleanly (first press shows the
// "press again to exit" hint) before the PTY is torn down.
await session.press(["ctrl", "c"]);
await session.press(["ctrl", "c"]);
await session.waitIdle({ timeout: 3_000 });
} catch {
// Session may already be dead; close() below still cleans up.
}
session.close();
}
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
it("shows the interactive chat view on launch", async () => {
const session = await launchCli();
await waitForChatView(session);
const screen = await session.text({ trimEnd: true });
expect(screen).toContain("What can I do for you?");
expect(screen).toContain("○ Plan ● Act (Tab)");
expect(screen).toContain("Auto-approve all enabled (Shift+Tab)");
});
it("toggles plan/act mode with Tab", async () => {
const session = await launchCli();
await waitForChatView(session);
expect(await session.text()).toContain("○ Plan ● Act (Tab)");
await session.press("tab");
// Reactive wait: resolves as soon as the toggled indicator renders.
await session.waitForText("● Plan ○ Act (Tab)", {
timeout: UI_TIMEOUT_MS,
});
// Unlike stream-grepping, the emulated screen reflects current state:
// the old indicator is gone, not just buried in scrollback.
const screen = await session.text();
expect(screen).toContain("● Plan ○ Act (Tab)");
expect(screen).not.toContain("○ Plan ● Act (Tab)");
});
it("toggles auto-approve-all with Shift+Tab", async () => {
const session = await launchCli();
await waitForChatView(session);
expect(await session.text()).toContain(
"Auto-approve all enabled (Shift+Tab)",
);
await session.press(["shift", "tab"]);
await session.waitForText("Auto-approve all disabled (Shift+Tab)", {
timeout: UI_TIMEOUT_MS,
});
const screen = await session.text();
expect(screen).not.toContain("Auto-approve all enabled (Shift+Tab)");
});
it("opens /settings, navigates tabs, and closes with Escape", async () => {
const session = await launchCli();
await waitForChatView(session);
await session.type("/settings");
// Slash menu completion for the settings command.
await session.waitForText("Modify agent configuration", {
timeout: UI_TIMEOUT_MS,
});
// A single Enter accepts the highlighted completion and submits it.
// (The `script`-based suite pressed Enter twice with 250ms sleeps; with
// reactive key delivery the second Enter would leak into the settings
// view and activate the focused row.)
await session.press("enter");
await session.waitForText("←/→ switch tabs", { timeout: UI_TIMEOUT_MS });
const settingsScreen = await session.text();
expect(settingsScreen).toContain("Settings");
expect(settingsScreen).toContain("▸ Provider");
// Switch from the General tab to the MCP tab; the body swaps from the
// provider/model rows to MCP content.
await session.press("right");
await session.text({
waitFor: (text) => !text.includes("Compaction"),
timeout: UI_TIMEOUT_MS,
});
await session.press("escape");
await session.waitForText("Use / for slash commands", {
timeout: UI_TIMEOUT_MS,
});
expect(await session.text()).not.toContain("←/→ switch tabs");
});
it("launches config view directly with `cline config`", async () => {
const session = await launchCli(["config"]);
await session.waitForText("←/→ switch tabs", {
timeout: LAUNCH_TIMEOUT_MS,
});
const screen = await session.text();
expect(screen).toContain("Settings");
expect(screen).toContain("▸ Provider");
});
it("dismisses the ClinePass promo with any key and marks it as shown", async () => {
// Re-enable the promo dialog that the shared env suppresses.
const env = createCliEnv({ CLINE_DISABLE_CLINE_PASS_NOTICE: undefined });
const dataDir = env.CLINE_DATA_DIR as string;
const session = await launchCli([], env);
await session.waitForText("Try ClinePass", { timeout: LAUNCH_TIMEOUT_MS });
await session.waitForText("Press Enter to open, any other key to close", {
timeout: UI_TIMEOUT_MS,
});
// Any key other than Enter dismisses the dialog (Esc is unreliable in
// some terminals, notably on Windows).
await session.type("x");
await session.text({
waitFor: (text) => !text.includes("Try ClinePass"),
timeout: UI_TIMEOUT_MS,
});
const screen = await session.text();
expect(screen).toContain("What can I do for you?");
expect(screen).not.toContain("Open ClinePass");
// The "shown" marker is persisted once the dialog is dismissed so the
// promo doesn't reappear on the next launch.
const markerPath = path.join(dataDir, "settings", "cli-notices.json");
await session.waitIdle({ timeout: UI_TIMEOUT_MS });
expect(existsSync(markerPath)).toBe(true);
expect(readFileSync(markerPath, "utf8")).toContain(
'"cline-cli-cline-pass-intro": true',
);
});
});
+1 -1
View File
@@ -10,9 +10,9 @@ import {
saveProviderOAuthCredentials,
} from "@cline/core";
import { Command } from "commander";
import open from "open";
import React from "react";
import { disableOpenTuiGraphicsProbe } from "../tui/opentui-env";
import open from "../utils/open";
import {
getPersistedProviderApiKey,
isOAuthProvider,
+5 -3
View File
@@ -1,10 +1,11 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { existsSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { basename, extname, join } from "node:path";
import {
type BuiltinToolAvailabilityContext,
createUserInstructionConfigService,
discoverPluginModulePaths,
getPluginDisplayName,
hasMcpSettingsFile,
listHookConfigFiles,
listPluginTools,
@@ -15,6 +16,7 @@ import {
type SkillConfig,
type WorkflowConfig,
} from "@cline/core";
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
import { Command } from "commander";
import { getToolCatalog } from "../runtime/tools";
import { loadInteractiveConfigData } from "../tui/interactive-config";
@@ -209,7 +211,7 @@ async function runAgentsConfigCommand(
continue;
}
const filePath = join(directory, entry.name);
const raw = readFileSync(filePath, "utf8");
const raw = readFileSyncStrippingUtf8Bom(filePath);
const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const frontmatter = frontmatterMatch?.[1] ?? "";
const nameMatch = frontmatter.match(/^\s*name:\s*(.+?)\s*$/m);
@@ -269,7 +271,7 @@ async function runPluginsConfigCommand(
continue;
}
pluginsByPath.set(filePath, {
name: basename(filePath, extname(filePath)),
name: getPluginDisplayName(filePath, directory),
path: filePath,
});
}
@@ -0,0 +1,304 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ConnectIo } from "../connectors/types";
const mocks = vi.hoisted(() => ({
ensureDetachedHubServer: vi.fn(),
readHubDiscovery: vi.fn(),
connect: vi.fn(),
command: vi.fn(),
close: vi.fn(),
clientOptions: vi.fn(),
}));
vi.mock("@cline/core", () => ({
ensureDetachedHubServer: mocks.ensureDetachedHubServer,
readHubDiscovery: mocks.readHubDiscovery,
resolveProductionHubOwnerContext: () => ({
ownerId: "hub-production",
discoveryPath: "/tmp/production.json",
}),
resolveSharedHubOwnerContext: () => ({
ownerId: "hub-owner",
discoveryPath: "/tmp/owner.json",
}),
NodeHubClient: class {
constructor(options: unknown) {
mocks.clientOptions(options);
}
connect = mocks.connect;
command = mocks.command;
close = mocks.close;
},
}));
import { startConnectorViaHub, stopConnectorsViaHub } from "./connect-via-hub";
describe("startConnectorViaHub", () => {
const io: ConnectIo = { writeln: vi.fn(), writeErr: vi.fn() };
beforeEach(() => {
vi.clearAllMocks();
mocks.ensureDetachedHubServer.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
authToken: "token",
});
mocks.readHubDiscovery.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
capabilities: ["session.create", "connector.start"],
});
mocks.connect.mockResolvedValue(undefined);
});
afterEach(() => {
vi.clearAllMocks();
});
function startRequest(overrides: Record<string, unknown> = {}) {
return {
channel: "slack",
instanceId: "cline-slack",
args: ["--bot-token", "xoxb"],
io,
cwd: "/workspace",
...overrides,
};
}
it("hands the start to the hub and reports supervision", async () => {
mocks.command.mockResolvedValue({
version: "v1",
ok: true,
payload: {
started: true,
record: { pid: 4242, state: "running" },
},
});
await expect(startConnectorViaHub(startRequest())).resolves.toEqual({
delegated: true,
exitCode: 0,
});
expect(mocks.command).toHaveBeenCalledWith("connector.start", {
channel: "slack",
instanceId: "cline-slack",
args: ["--bot-token", "xoxb"],
restart: false,
});
expect(io.writeln).toHaveBeenCalledWith(
expect.stringContaining("started under hub supervision pid=4242"),
);
expect(mocks.close).toHaveBeenCalled();
});
it("passes a restart through", async () => {
mocks.command.mockResolvedValue({
version: "v1",
ok: true,
payload: { started: true, record: { state: "running" } },
});
await startConnectorViaHub(startRequest({ restart: true }));
expect(mocks.command).toHaveBeenCalledWith(
"connector.start",
expect.objectContaining({ restart: true }),
);
});
it("treats an already-running instance as success", async () => {
mocks.command.mockResolvedValue({
version: "v1",
ok: true,
payload: {
started: false,
reason: "already_running",
record: { pid: 99, state: "running" },
},
});
await expect(startConnectorViaHub(startRequest())).resolves.toEqual({
delegated: true,
exitCode: 0,
});
expect(io.writeln).toHaveBeenCalledWith(
expect.stringContaining("already running under the hub"),
);
});
it("falls back when the hub cannot be reached", async () => {
mocks.ensureDetachedHubServer.mockRejectedValue(new Error("EADDRINUSE"));
const outcome = await startConnectorViaHub(startRequest());
expect(outcome.delegated).toBe(false);
expect(mocks.command).not.toHaveBeenCalled();
});
it("falls back when a running hub predates connector supervision", async () => {
// The normal state of a long-lived host mid-upgrade: a new CLI, an old hub.
mocks.readHubDiscovery.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
capabilities: ["session.create"],
});
const outcome = await startConnectorViaHub(startRequest());
expect(outcome).toEqual({
delegated: false,
reason: "hub does not support connector supervision",
});
expect(mocks.command).not.toHaveBeenCalled();
});
it("falls back when the hub reports supervision unavailable", async () => {
mocks.command.mockResolvedValue({
version: "v1",
ok: false,
error: {
code: "connector_command_failed",
message: "connector supervision is unavailable in this hub",
},
});
const outcome = await startConnectorViaHub(startRequest());
expect(outcome.delegated).toBe(false);
});
it("falls back when the hub command throws", async () => {
mocks.command.mockRejectedValue(new Error("socket closed"));
const outcome = await startConnectorViaHub(startRequest());
expect(outcome.delegated).toBe(false);
expect(mocks.close).toHaveBeenCalled();
});
it("surfaces a genuine start refusal instead of starting locally", async () => {
mocks.command.mockResolvedValue({
version: "v1",
ok: false,
error: {
code: "connector_command_failed",
message: "instanceId is required",
},
});
await expect(startConnectorViaHub(startRequest())).resolves.toEqual({
delegated: true,
exitCode: 1,
});
expect(io.writeErr).toHaveBeenCalledWith(
expect.stringContaining("hub refused to start slack"),
);
});
it("reports a hub that accepted the command but did not start anything", async () => {
mocks.command.mockResolvedValue({
version: "v1",
ok: true,
payload: {
started: false,
record: { state: "failed", lastError: "bad token" },
},
});
await expect(startConnectorViaHub(startRequest())).resolves.toEqual({
delegated: true,
exitCode: 1,
});
expect(io.writeErr).toHaveBeenCalledWith(
expect.stringContaining("bad token"),
);
});
});
describe("stopConnectorsViaHub", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.readHubDiscovery.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
capabilities: [
"connector.start",
"connector.stop",
"connector.supervised",
],
});
mocks.connect.mockResolvedValue(undefined);
});
it("retires every supervised instance of a channel", async () => {
mocks.command.mockImplementation(async (command: string) => {
if (command === "connector.supervised") {
return {
ok: true,
payload: {
supervised: [
{ channel: "slack", instanceId: "a" },
{ channel: "slack", instanceId: "b" },
{ channel: "telegram", instanceId: "c" },
],
},
};
}
return { ok: true, payload: { stopped: true } };
});
await expect(stopConnectorsViaHub({ channel: "slack" })).resolves.toBe(2);
expect(mocks.command).toHaveBeenCalledWith("connector.stop", {
channel: "slack",
instanceId: "a",
});
expect(mocks.command).toHaveBeenCalledWith("connector.stop", {
channel: "slack",
instanceId: "b",
});
// A different channel is left alone.
expect(mocks.command).not.toHaveBeenCalledWith("connector.stop", {
channel: "telegram",
instanceId: "c",
});
});
it("retires only the requested instance", async () => {
mocks.command.mockImplementation(async (command: string) => {
if (command === "connector.supervised") {
return {
ok: true,
payload: {
supervised: [
{ channel: "slack", instanceId: "a" },
{ channel: "slack", instanceId: "b" },
],
},
};
}
return { ok: true, payload: { stopped: true } };
});
await expect(
stopConnectorsViaHub({ channel: "slack", instanceId: "b" }),
).resolves.toBe(1);
expect(mocks.command).toHaveBeenCalledWith("connector.stop", {
channel: "slack",
instanceId: "b",
});
});
it("reports nothing to stop when the hub supervises none of them", async () => {
mocks.command.mockResolvedValue({ ok: true, payload: { supervised: [] } });
await expect(stopConnectorsViaHub({ channel: "slack" })).resolves.toBe(0);
});
it("returns undefined when the hub cannot supervise", async () => {
mocks.readHubDiscovery.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
capabilities: ["session.create"],
});
await expect(
stopConnectorsViaHub({ channel: "slack" }),
).resolves.toBeUndefined();
});
});
+289
View File
@@ -0,0 +1,289 @@
import {
ensureDetachedHubServer,
NodeHubClient,
readHubDiscovery,
resolveProductionHubOwnerContext,
resolveSharedHubOwnerContext,
} from "@cline/core";
import {
type ConnectorStartResult,
resolveClineBuildEnv,
type SupervisedConnectorRecord,
} from "@cline/shared";
import type { ConnectIo } from "../connectors/types";
/**
* Error codes that mean "this hub cannot supervise connectors", as opposed to
* "the start failed". Both are answers from the hub, but only the former should
* send the caller back to starting the connector itself.
*/
const UNSUPPORTED_ERROR_CODES = new Set([
"unsupported_command",
"unsupported_connector_command",
]);
const UNSUPPORTED_MESSAGE_FRAGMENT = "connector supervision is unavailable";
export type HubDelegationOutcome =
/** The hub owns the connector now; `exitCode` is the command's result. */
| { delegated: true; exitCode: number }
/** Nothing was started; the caller should start the connector locally. */
| { delegated: false; reason: string };
function resolveHubOwnerContext() {
return resolveClineBuildEnv() === "production"
? resolveProductionHubOwnerContext()
: resolveSharedHubOwnerContext();
}
/**
* Whether the hub at `url` advertises connector supervision.
*
* A newer CLI regularly talks to an older running hub — that is the normal state
* of a long-lived host mid-upgrade — and such a hub would reject
* `connector.start` outright. Checking the advertised capability first keeps that
* case on the local path instead of turning it into a failed start.
*/
async function hubSupportsSupervision(): Promise<boolean> {
try {
const owner = resolveHubOwnerContext();
const record = await readHubDiscovery(owner.discoveryPath);
return record?.capabilities?.includes("connector.start") === true;
} catch {
return false;
}
}
function describeRecord(record: SupervisedConnectorRecord | undefined): string {
if (!record) {
return "";
}
const details = [
record.pid === undefined ? undefined : `pid=${record.pid}`,
`state=${record.state}`,
].filter(Boolean);
return details.length > 0 ? ` ${details.join(" ")}` : "";
}
/**
* What the running hub is supervising, or undefined when it cannot say.
*
* Deliberately does not start a hub: this exists for diagnostics, and `cline
* doctor` reporting on the system must never change it.
*/
export async function listSupervisedConnectorsViaHub(): Promise<
SupervisedConnectorRecord[] | undefined
> {
let url: string;
let authToken: string | undefined;
try {
const owner = resolveHubOwnerContext();
const record = await readHubDiscovery(owner.discoveryPath);
if (
!record?.url ||
!record.capabilities?.includes("connector.supervised")
) {
return undefined;
}
url = record.url;
authToken = record.authToken;
} catch {
return undefined;
}
const client = new NodeHubClient({
url,
...(authToken ? { authToken } : {}),
clientType: "cli-doctor",
displayName: "doctor",
});
try {
await client.connect();
const reply = await client.command("connector.supervised");
if (!reply.ok) {
return undefined;
}
const supervised = (reply.payload as { supervised?: unknown })?.supervised;
return Array.isArray(supervised)
? (supervised as SupervisedConnectorRecord[])
: undefined;
} catch {
return undefined;
} finally {
try {
client.close();
} catch {
// One-shot connection; a failed close changes nothing.
}
}
}
/**
* Ask the hub to stop supervising a channel's connectors, or one instance of it.
*
* Returns how many the hub stopped, or undefined when it cannot supervise. The
* local stop path alone is not enough: it finds processes through their state
* files, so a connector that has not written one yet — still starting, or failing
* to start — would keep running under the hub and be restarted.
*/
export async function stopConnectorsViaHub(input: {
channel: string;
instanceId?: string;
}): Promise<number | undefined> {
const supervised = await listSupervisedConnectorsViaHub();
if (!supervised) {
return undefined;
}
const targets = supervised.filter(
(record) =>
record.channel === input.channel &&
(input.instanceId === undefined ||
record.instanceId === input.instanceId),
);
if (targets.length === 0) {
return 0;
}
let url: string;
let authToken: string | undefined;
try {
const owner = resolveHubOwnerContext();
const record = await readHubDiscovery(owner.discoveryPath);
if (!record?.url) {
return undefined;
}
url = record.url;
authToken = record.authToken;
} catch {
return undefined;
}
const client = new NodeHubClient({
url,
...(authToken ? { authToken } : {}),
clientType: "cli-connect",
displayName: `stop ${input.channel}`,
});
let stopped = 0;
try {
await client.connect();
for (const target of targets) {
const reply = await client.command("connector.stop", {
channel: target.channel,
instanceId: target.instanceId,
});
if (reply.ok) {
stopped += 1;
}
}
return stopped;
} catch {
return stopped > 0 ? stopped : undefined;
} finally {
try {
client.close();
} catch {
// One-shot connection; a failed close changes nothing.
}
}
}
/**
* Ask the hub to start and own a connector.
*
* The hub spawning the connector — rather than the connector spawning itself and
* then bringing up a hub — is what makes the hub the single authority on how many
* processes hold one connector's credentials, and what lets it reap and restart
* them when they die. Every failure mode here falls back to the local path so a
* missing or older hub cannot stop a connector from starting.
*/
export async function startConnectorViaHub(input: {
channel: string;
instanceId: string;
args: string[];
restart?: boolean;
io: ConnectIo;
cwd?: string;
}): Promise<HubDelegationOutcome> {
const cwd = input.cwd ?? process.cwd();
let hub: { url: string; authToken: string };
try {
hub = await ensureDetachedHubServer(cwd);
} catch (error) {
return {
delegated: false,
reason: `hub unavailable: ${
error instanceof Error ? error.message : String(error)
}`,
};
}
if (!(await hubSupportsSupervision())) {
return {
delegated: false,
reason: "hub does not support connector supervision",
};
}
const client = new NodeHubClient({
url: hub.url,
authToken: hub.authToken,
clientType: "cli-connect",
displayName: `connect ${input.channel}`,
cwd,
});
try {
await client.connect();
const reply = await client.command("connector.start", {
channel: input.channel,
instanceId: input.instanceId,
args: input.args,
restart: input.restart === true,
});
if (!reply.ok) {
const code = reply.error?.code ?? "";
const message = reply.error?.message ?? "connector start failed";
if (
UNSUPPORTED_ERROR_CODES.has(code) ||
message.includes(UNSUPPORTED_MESSAGE_FRAGMENT)
) {
return { delegated: false, reason: message };
}
input.io.writeErr(
`[connect] hub refused to start ${input.channel}: ${message}`,
);
return { delegated: true, exitCode: 1 };
}
const payload = reply.payload as ConnectorStartResult | undefined;
const record = payload?.record;
if (payload?.started === false && payload.reason === "already_running") {
input.io.writeln(
`[connect] ${input.channel} connector ${input.instanceId} is already running under the hub${describeRecord(record)}`,
);
return { delegated: true, exitCode: 0 };
}
if (payload?.started !== true) {
input.io.writeErr(
`[connect] hub could not start ${input.channel} connector ${input.instanceId}${
record?.lastError ? `: ${record.lastError}` : ""
}`,
);
return { delegated: true, exitCode: 1 };
}
input.io.writeln(
`[connect] ${input.channel} connector ${input.instanceId} started under hub supervision${describeRecord(record)}`,
);
input.io.writeln(
"[connect] the hub will restart it if it exits; use `cline connect --stop` to retire it",
);
return { delegated: true, exitCode: 0 };
} catch (error) {
return {
delegated: false,
reason: `hub command failed: ${
error instanceof Error ? error.message : String(error)
}`,
};
} finally {
try {
client.close();
} catch {
// The connection is one-shot; a failed close changes nothing.
}
}
}
+694
View File
@@ -0,0 +1,694 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
CLINE_CONNECTOR_DETACHED_CHILD_ENV,
CONNECT_ALREADY_RUNNING_EXIT_CODE,
} from "../connectors/common";
import type { ConnectIo, ConnectRunContext } from "../connectors/types";
import {
runCleanupConnectorInstance,
runConnectAdapter,
runRestartConnector,
runStopAllConnectors,
stopAllConnectors,
} from "./connect";
const mocks = vi.hoisted(() => ({
startConnectorViaHub: vi.fn(),
stopConnectorsViaHub: vi.fn(async () => undefined as number | undefined),
disableConnectorAutostart: vi.fn(),
getPersistedConnectorConnection: vi.fn(),
getConnector: vi.fn(),
listActiveConnectors: vi.fn(),
listConnectors: vi.fn((): Array<{ name: string; description: string }> => []),
persistConnectorConnection: vi.fn(),
removePersistedConnectorConnection: vi.fn(),
run: vi.fn(),
validate: vi.fn(),
}));
vi.mock("@cline/core", () => ({
disableConnectorAutostart: mocks.disableConnectorAutostart,
getPersistedConnectorConnection: mocks.getPersistedConnectorConnection,
listActiveConnectors: mocks.listActiveConnectors,
persistConnectorConnection: mocks.persistConnectorConnection,
removePersistedConnectorConnection: mocks.removePersistedConnectorConnection,
}));
vi.mock("../connectors/registry", () => ({
getConnector: mocks.getConnector,
listConnectors: mocks.listConnectors,
}));
vi.mock("./connect-via-hub", () => ({
startConnectorViaHub: mocks.startConnectorViaHub,
stopConnectorsViaHub: mocks.stopConnectorsViaHub,
}));
describe("runConnectAdapter", () => {
const previousDetachedChild = process.env[CLINE_CONNECTOR_DETACHED_CHILD_ENV];
const io: ConnectIo = {
writeln: vi.fn(),
writeErr: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
mocks.listConnectors.mockReturnValue([]);
mocks.listActiveConnectors.mockReturnValue([]);
mocks.run.mockImplementation(
async (_args: string[], _io: ConnectIo, context: ConnectRunContext) => {
context.setPersistenceInstanceId("cline_bot");
return 0;
},
);
mocks.validate.mockResolvedValue(0);
mocks.getConnector.mockResolvedValue({
name: "telegram",
description: "Telegram",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
});
});
afterEach(() => {
if (previousDetachedChild === undefined) {
delete process.env[CLINE_CONNECTOR_DETACHED_CHILD_ENV];
} else {
process.env[CLINE_CONNECTOR_DETACHED_CHILD_ENV] = previousDetachedChild;
}
});
it("persists a successful detached connector start", async () => {
await expect(
runConnectAdapter("telegram", ["-k", "token"], io),
).resolves.toBe(0);
expect(mocks.persistConnectorConnection).toHaveBeenCalledWith(
"telegram",
"cline_bot",
["-k", "token"],
);
expect(mocks.disableConnectorAutostart).not.toHaveBeenCalled();
});
it("persists a successful env-only connector start", async () => {
await expect(runConnectAdapter("telegram", [], io)).resolves.toBe(0);
expect(mocks.persistConnectorConnection).toHaveBeenCalledWith(
"telegram",
"cline_bot",
[],
);
expect(mocks.disableConnectorAutostart).not.toHaveBeenCalled();
});
it("persists connector-resolved launch arguments", async () => {
mocks.run.mockImplementation(
async (_args: string[], _io: ConnectIo, context: ConnectRunContext) => {
context.setPersistenceInstanceId("resolved_bot");
context.setPersistenceArgs([
"--bot-token",
"token",
"--bot-username",
"resolved_bot",
]);
return 0;
},
);
await expect(
runConnectAdapter("telegram", ["--bot-token", "token"], io),
).resolves.toBe(0);
expect(mocks.persistConnectorConnection).toHaveBeenCalledWith(
"telegram",
"resolved_bot",
["--bot-token", "token", "--bot-username", "resolved_bot"],
);
});
it("does not rewrite persistence when a connector is already running", async () => {
mocks.run.mockResolvedValue(CONNECT_ALREADY_RUNNING_EXIT_CODE);
await expect(
runConnectAdapter("telegram", ["-k", "token"], io),
).resolves.toBe(0);
expect(mocks.persistConnectorConnection).not.toHaveBeenCalled();
expect(mocks.disableConnectorAutostart).not.toHaveBeenCalled();
});
it.each([
"-i",
"--interactive",
])("disables autostart after a successful %s foreground run exits", async (interactiveFlag) => {
await expect(
runConnectAdapter("telegram", ["-k", "token", interactiveFlag], io),
).resolves.toBe(0);
expect(mocks.persistConnectorConnection).not.toHaveBeenCalled();
expect(mocks.disableConnectorAutostart).toHaveBeenCalledWith(
"telegram",
"cline_bot",
);
});
it("does not change persistence after a failed foreground run", async () => {
mocks.run.mockResolvedValue(1);
await expect(
runConnectAdapter("telegram", ["-k", "token", "-i"], io),
).resolves.toBe(1);
expect(mocks.persistConnectorConnection).not.toHaveBeenCalled();
expect(mocks.disableConnectorAutostart).not.toHaveBeenCalled();
});
it("does not persist a failed detached launch", async () => {
mocks.run.mockResolvedValue(1);
await expect(
runConnectAdapter("telegram", ["-k", "token"], io),
).resolves.toBe(1);
expect(mocks.persistConnectorConnection).not.toHaveBeenCalled();
expect(mocks.disableConnectorAutostart).not.toHaveBeenCalled();
});
it("leaves persistence unchanged when an internal detached child exits", async () => {
process.env[CLINE_CONNECTOR_DETACHED_CHILD_ENV] = "1";
await expect(
runConnectAdapter("telegram", ["-k", "token", "-i"], io),
).resolves.toBe(0);
expect(mocks.persistConnectorConnection).not.toHaveBeenCalled();
expect(mocks.disableConnectorAutostart).not.toHaveBeenCalled();
});
it("does not persist help invocations", async () => {
await expect(runConnectAdapter("telegram", ["--help"], io)).resolves.toBe(
0,
);
expect(mocks.persistConnectorConnection).not.toHaveBeenCalled();
expect(mocks.disableConnectorAutostart).not.toHaveBeenCalled();
});
it("leaves autostart unchanged during shared process cleanup", async () => {
const stopAll = vi.fn().mockResolvedValue({
stoppedProcesses: 1,
failedProcesses: 0,
stoppedSessions: 2,
});
mocks.listConnectors.mockReturnValue([
{ name: "telegram", description: "Telegram" },
]);
mocks.getConnector.mockResolvedValue({
name: "telegram",
description: "Telegram",
run: mocks.run,
showHelp: vi.fn(),
stopAll,
});
await expect(stopAllConnectors(io)).resolves.toEqual({
stoppedProcesses: 1,
failedProcesses: 0,
stoppedSessions: 2,
executed: 1,
});
expect(stopAll).toHaveBeenCalledWith(io);
expect(mocks.disableConnectorAutostart).not.toHaveBeenCalled();
});
it("disables autostart for an explicit stop-all command", async () => {
const stopAll = vi.fn().mockResolvedValue({
stoppedProcesses: 1,
failedProcesses: 0,
stoppedSessions: 2,
});
mocks.listConnectors.mockReturnValue([
{ name: "telegram", description: "Telegram" },
]);
mocks.getConnector.mockResolvedValue({
name: "telegram",
description: "Telegram",
run: mocks.run,
showHelp: vi.fn(),
stopAll,
});
await expect(runStopAllConnectors(io)).resolves.toBe(0);
expect(stopAll).toHaveBeenCalledWith(io);
expect(mocks.disableConnectorAutostart).toHaveBeenCalledWith();
});
it("validates a replacement before stopping the active instance", async () => {
const stopInstance = vi.fn().mockResolvedValue({
stoppedProcesses: 1,
failedProcesses: 0,
stoppedSessions: 0,
});
mocks.validate.mockResolvedValue(1);
mocks.listActiveConnectors.mockReturnValue([
{
id: "telegram:cline_bot",
type: "telegram",
instanceId: "cline_bot",
pid: 123,
hubUrl: "ws://127.0.0.1:4317",
botUsername: "cline_bot",
},
]);
mocks.getConnector.mockResolvedValue({
name: "telegram",
description: "Telegram",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
stopInstance,
});
await expect(
runRestartConnector("telegram", ["-k", "bad-token"], io),
).resolves.toBe(1);
expect(mocks.validate).toHaveBeenCalledWith(["-k", "bad-token"], io);
expect(stopInstance).not.toHaveBeenCalled();
expect(mocks.run).not.toHaveBeenCalled();
});
it("shows restart help without stopping an active instance", async () => {
const stopInstance = vi.fn().mockResolvedValue({
stoppedProcesses: 1,
failedProcesses: 0,
stoppedSessions: 0,
});
mocks.listActiveConnectors.mockReturnValue([
{
id: "telegram:cline_bot",
type: "telegram",
instanceId: "cline_bot",
pid: 123,
hubUrl: "ws://127.0.0.1:4317",
botUsername: "cline_bot",
},
]);
mocks.getConnector.mockResolvedValue({
name: "telegram",
description: "Telegram",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
stopInstance,
});
await expect(runRestartConnector("telegram", ["--help"], io)).resolves.toBe(
0,
);
expect(mocks.run).toHaveBeenCalledWith(["--help"], io, expect.any(Object));
expect(mocks.validate).not.toHaveBeenCalled();
expect(stopInstance).not.toHaveBeenCalled();
});
it("restores the last successful launch when a replacement fails", async () => {
const stopInstance = vi.fn().mockResolvedValue({
stoppedProcesses: 1,
failedProcesses: 0,
stoppedSessions: 0,
});
mocks.listActiveConnectors.mockReturnValue([
{
id: "telegram:cline_bot",
type: "telegram",
instanceId: "cline_bot",
pid: 123,
hubUrl: "ws://127.0.0.1:4317",
botUsername: "cline_bot",
},
]);
mocks.getPersistedConnectorConnection.mockReturnValue({
channel: "telegram",
instanceId: "cline_bot",
connectArgs: ["-k", "new-token"],
lastSuccessfulArgs: ["-k", "old-token"],
enabled: true,
updatedAt: "2026-07-25T00:00:00.000Z",
lastConnectedAt: "2026-07-25T00:00:00.000Z",
});
mocks.run
.mockResolvedValueOnce(1)
.mockImplementationOnce(
async (_args: string[], _io: ConnectIo, context: ConnectRunContext) => {
context.setPersistenceInstanceId("cline_bot");
return 0;
},
);
mocks.getConnector.mockResolvedValue({
name: "telegram",
description: "Telegram",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
stopInstance,
});
await expect(
runRestartConnector("telegram", ["-k", "new-token"], io),
).resolves.toBe(1);
expect(stopInstance).toHaveBeenCalledWith("cline_bot", io);
expect(mocks.run).toHaveBeenNthCalledWith(
1,
["-k", "new-token"],
io,
expect.any(Object),
);
expect(mocks.run).toHaveBeenNthCalledWith(
2,
["-k", "old-token"],
io,
expect.any(Object),
);
expect(mocks.disableConnectorAutostart).not.toHaveBeenCalled();
expect(mocks.persistConnectorConnection).toHaveBeenCalledWith(
"telegram",
"cline_bot",
["-k", "old-token"],
);
});
it("restarts an active instance without persisted rollback arguments", async () => {
const stopInstance = vi.fn().mockResolvedValue({
stoppedProcesses: 1,
failedProcesses: 0,
stoppedSessions: 0,
});
mocks.listActiveConnectors.mockReturnValue([
{
id: "telegram:cline_bot",
type: "telegram",
instanceId: "cline_bot",
pid: 123,
hubUrl: "ws://127.0.0.1:4317",
botUsername: "cline_bot",
},
]);
mocks.getPersistedConnectorConnection.mockReturnValue(undefined);
mocks.getConnector.mockResolvedValue({
name: "telegram",
description: "Telegram",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
stopInstance,
});
await expect(
runRestartConnector("telegram", ["-k", "new-token"], io),
).resolves.toBe(0);
expect(stopInstance).toHaveBeenCalledWith("cline_bot", io);
expect(mocks.run).toHaveBeenCalledWith(
["-k", "new-token"],
io,
expect.any(Object),
);
});
it("does not start a replacement when the active process cannot be stopped", async () => {
const stopInstance = vi.fn().mockResolvedValue({
stoppedProcesses: 0,
failedProcesses: 1,
stoppedSessions: 0,
});
mocks.listActiveConnectors.mockReturnValue([
{
id: "telegram:cline_bot",
type: "telegram",
instanceId: "cline_bot",
pid: 123,
hubUrl: "ws://127.0.0.1:4317",
botUsername: "cline_bot",
},
]);
mocks.getConnector.mockResolvedValue({
name: "telegram",
description: "Telegram",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
stopInstance,
});
await expect(
runRestartConnector("telegram", ["-k", "new-token"], io),
).resolves.toBe(1);
expect(stopInstance).toHaveBeenCalledWith("cline_bot", io);
expect(mocks.run).not.toHaveBeenCalled();
});
it("does not count an already-running instance as a successful replacement", async () => {
const stopInstance = vi.fn().mockResolvedValue({
stoppedProcesses: 1,
failedProcesses: 0,
stoppedSessions: 0,
});
mocks.listActiveConnectors.mockReturnValue([
{
id: "telegram:cline_bot",
type: "telegram",
instanceId: "cline_bot",
pid: 123,
hubUrl: "ws://127.0.0.1:4317",
botUsername: "cline_bot",
},
]);
mocks.getPersistedConnectorConnection.mockReturnValue({
channel: "telegram",
instanceId: "cline_bot",
connectArgs: ["-k", "new-token"],
lastSuccessfulArgs: ["-k", "old-token"],
enabled: true,
updatedAt: "2026-07-25T00:00:00.000Z",
lastConnectedAt: "2026-07-25T00:00:00.000Z",
});
mocks.run.mockResolvedValue(CONNECT_ALREADY_RUNNING_EXIT_CODE);
mocks.getConnector.mockResolvedValue({
name: "telegram",
description: "Telegram",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
stopInstance,
});
await expect(
runRestartConnector("telegram", ["-k", "new-token"], io),
).resolves.toBe(1);
expect(mocks.run).toHaveBeenCalledTimes(1);
expect(io.writeErr).toHaveBeenCalledWith(
"[connect] replacement was not started because telegram instance cline_bot is still running",
);
expect(mocks.persistConnectorConnection).not.toHaveBeenCalled();
});
});
describe("runCleanupConnectorInstance", () => {
const io: ConnectIo = {
writeln: vi.fn(),
writeErr: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
it("reaps one instance without disabling its autostart", async () => {
const stopInstance = vi.fn().mockResolvedValue({
stoppedProcesses: 0,
failedProcesses: 0,
stoppedSessions: 2,
});
mocks.getConnector.mockResolvedValue({
name: "slack",
description: "Slack",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
stopInstance,
});
await expect(
runCleanupConnectorInstance("slack", "cline-slack", io),
).resolves.toBe(0);
expect(stopInstance).toHaveBeenCalledWith("cline-slack", io);
// The instance crashed; it was not retired. Disabling autostart here would
// make every crash silently opt the connector out of supervision.
expect(mocks.disableConnectorAutostart).not.toHaveBeenCalled();
});
it("reports a failed reap", async () => {
mocks.getConnector.mockResolvedValue({
name: "slack",
description: "Slack",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
stopInstance: vi.fn().mockResolvedValue({
stoppedProcesses: 0,
failedProcesses: 1,
stoppedSessions: 0,
}),
});
await expect(
runCleanupConnectorInstance("slack", "cline-slack", io),
).resolves.toBe(1);
});
it("rejects an adapter without per-instance stop", async () => {
mocks.getConnector.mockResolvedValue({
name: "slack",
description: "Slack",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
});
await expect(
runCleanupConnectorInstance("slack", "cline-slack", io),
).resolves.toBe(1);
expect(io.writeErr).toHaveBeenCalledWith(
'connect adapter "slack" does not support per-instance stop',
);
});
it("rejects an unknown adapter", async () => {
mocks.getConnector.mockResolvedValue(undefined);
await expect(
runCleanupConnectorInstance("nope", "instance", io),
).resolves.toBe(1);
expect(io.writeErr).toHaveBeenCalledWith('unknown connect adapter "nope"');
});
});
describe("hub-delegated connector starts", () => {
const io: ConnectIo = { writeln: vi.fn(), writeErr: vi.fn() };
beforeEach(() => {
vi.clearAllMocks();
mocks.listConnectors.mockReturnValue([]);
mocks.listActiveConnectors.mockReturnValue([]);
mocks.validate.mockResolvedValue(0);
mocks.run.mockResolvedValue(0);
mocks.getConnector.mockResolvedValue({
name: "slack",
description: "Slack",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
resolveInstanceId: () => "cline-slack",
});
mocks.startConnectorViaHub.mockResolvedValue({
delegated: true,
exitCode: 0,
});
});
afterEach(() => {
delete process.env.CLINE_CONNECTOR_SUPERVISED;
});
it("asks the hub to own a background connector and records the intent", async () => {
await expect(
runConnectAdapter("slack", ["--bot-token", "xoxb"], io),
).resolves.toBe(0);
expect(mocks.startConnectorViaHub).toHaveBeenCalledWith(
expect.objectContaining({
channel: "slack",
instanceId: "cline-slack",
args: ["--bot-token", "xoxb"],
}),
);
// The adapter must not also run here: the hub owns the process now.
expect(mocks.run).not.toHaveBeenCalled();
expect(mocks.persistConnectorConnection).toHaveBeenCalledWith(
"slack",
"cline-slack",
["--bot-token", "xoxb"],
);
});
it("runs locally for a foreground connector", async () => {
await runConnectAdapter("slack", ["--bot-token", "xoxb", "-i"], io);
expect(mocks.startConnectorViaHub).not.toHaveBeenCalled();
expect(mocks.run).toHaveBeenCalled();
});
it("runs locally inside a supervised process instead of asking the hub again", async () => {
process.env.CLINE_CONNECTOR_SUPERVISED = "1";
await runConnectAdapter("slack", ["--bot-token", "xoxb"], io);
// Delegating here would send the hub straight back to spawning this same
// process.
expect(mocks.startConnectorViaHub).not.toHaveBeenCalled();
expect(mocks.run).toHaveBeenCalled();
});
it("runs locally when the instance id cannot be known up front", async () => {
mocks.getConnector.mockResolvedValue({
name: "telegram",
description: "Telegram",
run: mocks.run,
validate: mocks.validate,
showHelp: vi.fn(),
resolveInstanceId: () => undefined,
});
await runConnectAdapter("telegram", ["-k", "token"], io);
expect(mocks.startConnectorViaHub).not.toHaveBeenCalled();
expect(mocks.run).toHaveBeenCalled();
});
it("falls back to a local start when the hub declines", async () => {
mocks.startConnectorViaHub.mockResolvedValue({
delegated: false,
reason: "hub does not support connector supervision",
});
await runConnectAdapter("slack", ["--bot-token", "xoxb"], io);
expect(mocks.run).toHaveBeenCalled();
});
it("does not validate or delegate a help invocation", async () => {
await runConnectAdapter("slack", ["--help"], io);
expect(mocks.startConnectorViaHub).not.toHaveBeenCalled();
expect(mocks.validate).not.toHaveBeenCalled();
});
it("reports a validation failure without contacting the hub", async () => {
mocks.validate.mockResolvedValue(2);
await expect(
runConnectAdapter("slack", ["--bot-token", "bad"], io),
).resolves.toBe(2);
expect(mocks.startConnectorViaHub).not.toHaveBeenCalled();
expect(mocks.run).not.toHaveBeenCalled();
});
});
+344 -14
View File
@@ -1,10 +1,34 @@
import {
disableConnectorAutostart,
getPersistedConnectorConnection,
listActiveConnectors,
persistConnectorConnection,
removePersistedConnectorConnection,
} from "@cline/core";
import {
isSupervisedConnectorProcess,
setStartingConnectorInstance,
} from "@cline/shared";
import {
CLINE_CONNECTOR_DETACHED_CHILD_ENV,
CONNECT_ALREADY_RUNNING_EXIT_CODE,
} from "../connectors/common";
import { getConnector, listConnectors } from "../connectors/registry";
import type { ConnectIo, ConnectStopResult } from "../connectors/types";
import type {
ConnectIo,
ConnectRunContext,
ConnectStopResult,
} from "../connectors/types";
import { startConnectorViaHub, stopConnectorsViaHub } from "./connect-via-hub";
const HELP_FLAGS = new Set(["-h", "--help"]);
const INTERACTIVE_FLAGS = new Set(["-i", "--interactive"]);
export async function stopAllConnectors(
io: ConnectIo,
): Promise<ConnectStopResult & { executed: number }> {
let stoppedProcesses = 0;
let failedProcesses = 0;
let stoppedSessions = 0;
let executed = 0;
for (const entry of listConnectors()) {
@@ -18,42 +42,339 @@ export async function stopAllConnectors(
executed += 1;
const result = await connector.stopAll(io);
stoppedProcesses += result.stoppedProcesses;
failedProcesses += result.failedProcesses;
stoppedSessions += result.stoppedSessions;
}
return { stoppedProcesses, stoppedSessions, executed };
return { stoppedProcesses, failedProcesses, stoppedSessions, executed };
}
export async function runStopAllConnectors(io: ConnectIo): Promise<number> {
const { stoppedProcesses, stoppedSessions, executed } =
const { stoppedProcesses, failedProcesses, stoppedSessions, executed } =
await stopAllConnectors(io);
if (executed === 0) {
io.writeln("[connect] no adapters support stop yet");
return 0;
}
disableConnectorAutostart();
io.writeln(
`[connect] stopped processes=${stoppedProcesses} sessions=${stoppedSessions}`,
`[connect] stopped processes=${stoppedProcesses} failed=${failedProcesses} sessions=${stoppedSessions}`,
);
return 0;
return failedProcesses === 0 ? 0 : 1;
}
export async function runStopConnector(
adapterName: string,
io: ConnectIo,
options: {
autostart: "disable" | "preserve";
instanceId?: string;
} = {
autostart: "disable",
},
): Promise<number> {
const connector = await getConnector(adapterName);
if (!connector) {
io.writeErr(`unknown connect adapter "${adapterName}"`);
return 1;
}
const stop = options.instanceId
? connector.stopInstance
? () => connector.stopInstance?.(options.instanceId ?? "", io)
: undefined
: connector.stopAll
? () => connector.stopAll?.(io)
: undefined;
if (!stop) {
io.writeErr(`connect adapter "${adapterName}" does not support stop`);
return 1;
}
// Retire it with the hub first. The local stop below finds processes through
// their state files, so a supervised connector that has not written one yet
// would survive and be restarted.
const stoppedByHub = await stopConnectorsViaHub({
channel: connector.name,
...(options.instanceId === undefined
? {}
: { instanceId: options.instanceId }),
});
if (stoppedByHub) {
io.writeln(
`[connect] hub stopped supervising ${stoppedByHub} ${connector.name} connector${stoppedByHub === 1 ? "" : "s"}`,
);
}
const result = await stop();
if (!result) {
io.writeErr(`connect adapter "${adapterName}" does not support stop`);
return 1;
}
if (options.autostart === "disable") {
disableConnectorAutostart(connector.name, options.instanceId);
}
io.writeln(
`[connect] ${connector.name}${options.instanceId ? ` instance=${options.instanceId}` : ""} stopped processes=${result.stoppedProcesses} failed=${result.failedProcesses} sessions=${result.stoppedSessions}`,
);
return result.failedProcesses === 0 ? 0 : 1;
}
/**
* Reap one connector instance that is no longer running.
*
* Invoked by the hub supervisor when it observes a connector die. It clears the
* same things a normal stop does — process state file, thread→session bindings,
* the instance's hub sessions — but deliberately leaves the autostart record
* intact: the instance crashed, it was not retired, so the supervisor still
* intends to restart it. `runStopConnector` with `autostart: "disable"` would
* make every crash silently opt the connector out of recovery.
*/
export async function runCleanupConnectorInstance(
adapterName: string,
instanceId: string,
io: ConnectIo,
): Promise<number> {
const connector = await getConnector(adapterName);
if (!connector) {
io.writeErr(`unknown connect adapter "${adapterName}"`);
return 1;
}
if (!connector.stopAll) {
io.writeErr(`connect adapter "${adapterName}" does not support stop`);
if (!connector.stopInstance) {
io.writeErr(
`connect adapter "${adapterName}" does not support per-instance stop`,
);
return 1;
}
const result: ConnectStopResult = await connector.stopAll(io);
const result = await connector.stopInstance(instanceId, io);
io.writeln(
`[connect] ${connector.name} stopped processes=${result.stoppedProcesses} sessions=${result.stoppedSessions}`,
`[connect] ${connector.name} instance=${instanceId} cleaned processes=${result.stoppedProcesses} failed=${result.failedProcesses} sessions=${result.stoppedSessions}`,
);
return 0;
return result.failedProcesses === 0 ? 0 : 1;
}
export async function runRestartConnector(
adapterName: string,
passthroughArgs: string[],
io: ConnectIo,
requestedInstanceId?: string,
): Promise<number> {
if (passthroughArgs.some((arg) => HELP_FLAGS.has(arg))) {
return await runConnectAdapter(adapterName, passthroughArgs, io);
}
const connector = await getConnector(adapterName);
if (!connector) {
io.writeErr(`unknown connect adapter "${adapterName}"`);
return 1;
}
const activeInstances = listActiveConnectors().filter(
(record) => record.type === adapterName,
);
if (!requestedInstanceId && activeInstances.length > 1) {
io.writeErr(
`cannot safely restart ${adapterName}: ${activeInstances.length} instances are active; specify an instance`,
);
return 1;
}
const instanceId = requestedInstanceId ?? activeInstances[0]?.instanceId;
const targetIsActive =
instanceId !== undefined &&
activeInstances.some((record) => record.instanceId === instanceId);
if (!targetIsActive || !instanceId) {
return await runConnectAdapter(adapterName, passthroughArgs, io);
}
const validationExitCode = await connector.validate(passthroughArgs, io);
if (validationExitCode !== 0) {
return validationExitCode;
}
// The supervisor replaces an instance in one step, so let it do the whole
// restart rather than stopping here and racing it to start the replacement.
// Only when the target is the instance these arguments describe: a
// `--restart-instance` pointing elsewhere is not ours to reinterpret.
if (
requestedInstanceId === undefined ||
connector.resolveInstanceId?.(passthroughArgs) === requestedInstanceId
) {
const delegated = await tryDelegateToHub(connector, passthroughArgs, io, {
restart: true,
});
if (delegated !== undefined) {
return delegated;
}
}
const previousConnection = getPersistedConnectorConnection(
adapterName,
instanceId,
);
const stopExitCode = await runStopConnector(adapterName, io, {
autostart: "preserve",
instanceId,
});
if (stopExitCode !== 0) {
return stopExitCode;
}
const replacement = await runConnectAdapterWithResult(
adapterName,
passthroughArgs,
io,
);
if (replacement.exitCode === 0) {
if (replacement.instanceId && replacement.instanceId !== instanceId) {
removePersistedConnectorConnection(adapterName, instanceId);
}
return 0;
}
if (replacement.exitCode === CONNECT_ALREADY_RUNNING_EXIT_CODE) {
io.writeErr(
`[connect] replacement was not started because ${adapterName} instance ${instanceId} is still running`,
);
return 1;
}
if (!previousConnection) {
io.writeErr(
`[connect] replacement failed and ${adapterName} instance ${instanceId} has no successful launch arguments for rollback`,
);
return replacement.exitCode;
}
io.writeErr(
`[connect] replacement failed; restoring ${adapterName} instance ${instanceId}`,
);
const rollback = await runConnectAdapterWithResult(
adapterName,
previousConnection.lastSuccessfulArgs,
io,
);
if (rollback.exitCode === 0) {
io.writeln(`[connect] restored ${adapterName} instance ${instanceId}`);
} else {
io.writeErr(
`[connect] failed to restore ${adapterName} instance ${instanceId}`,
);
}
return replacement.exitCode;
}
interface ConnectAdapterResult {
exitCode: number;
instanceId?: string;
}
async function runConnectAdapterWithResult(
adapterName: string,
passthroughArgs: string[],
io: ConnectIo,
): Promise<ConnectAdapterResult> {
const connector = await getConnector(adapterName);
if (!connector) {
io.writeErr(`unknown connect adapter "${adapterName}"`);
return { exitCode: 1 };
}
let persistenceArgs = passthroughArgs;
let persistenceInstanceId: string | undefined;
const context: ConnectRunContext = {
setPersistenceArgs: (args) => {
persistenceArgs = [...args];
},
setPersistenceInstanceId: (instanceId) => {
persistenceInstanceId = instanceId;
// Adapters report their instance id before they build a Cline core, so
// this lands in the environment before the hub daemon is spawned and
// inherited by it. Without it the daemon's autostart pass cannot tell
// that this instance is mid-startup and launches a second copy of it.
setStartingConnectorInstance({
channel: connector.name,
instanceId,
});
},
};
const exitCode = await connector.run(passthroughArgs, io, context);
if (exitCode === CONNECT_ALREADY_RUNNING_EXIT_CODE) {
return { exitCode, instanceId: persistenceInstanceId };
}
const isHelpInvocation = passthroughArgs.some((arg) => HELP_FLAGS.has(arg));
const isInteractiveInvocation = passthroughArgs.some((arg) =>
INTERACTIVE_FLAGS.has(arg),
);
// A supervised process is the hub's own connector, not a user invocation, so
// it makes the same autostart bookkeeping choices as a detached child: the
// process that asked for the start already recorded the intent.
const isDetachedChild =
process.env[CLINE_CONNECTOR_DETACHED_CHILD_ENV] === "1" ||
isSupervisedConnectorProcess();
if (
exitCode === 0 &&
!isHelpInvocation &&
!isDetachedChild &&
isInteractiveInvocation
) {
disableConnectorAutostart(connector.name, persistenceInstanceId);
} else if (
exitCode === 0 &&
!isHelpInvocation &&
!isDetachedChild &&
persistenceInstanceId
) {
persistConnectorConnection(
connector.name,
persistenceInstanceId,
persistenceArgs,
);
}
return { exitCode, instanceId: persistenceInstanceId };
}
/**
* Hand a background connector start to the hub, when that is possible.
*
* Returns the exit code once the hub owns the connector, or undefined to mean
* "start it locally instead". Delegation is skipped for foreground (`-i`) runs,
* which are attached to the user's terminal, and for connectors the hub itself
* launched, which would otherwise ask the hub to start them again.
*/
async function tryDelegateToHub(
connector: {
name: string;
validate: (args: string[], io: ConnectIo) => Promise<number>;
resolveInstanceId?: (args: string[]) => string | undefined;
},
passthroughArgs: string[],
io: ConnectIo,
options: { restart?: boolean } = {},
): Promise<number | undefined> {
if (
passthroughArgs.some((arg) => HELP_FLAGS.has(arg)) ||
passthroughArgs.some((arg) => INTERACTIVE_FLAGS.has(arg)) ||
process.env[CLINE_CONNECTOR_DETACHED_CHILD_ENV] === "1" ||
isSupervisedConnectorProcess()
) {
return undefined;
}
// Without an instance id the hub cannot enforce one process per connector,
// which is the entire point of routing through it.
const instanceId = connector.resolveInstanceId?.(passthroughArgs);
if (!instanceId) {
return undefined;
}
// Check the arguments here rather than after handing off: a bad token should
// fail in front of the user instead of becoming a supervised crash loop.
const validationExitCode = await connector.validate(passthroughArgs, io);
if (validationExitCode !== 0) {
return validationExitCode;
}
const outcome = await startConnectorViaHub({
channel: connector.name,
instanceId,
args: passthroughArgs,
...(options.restart === undefined ? {} : { restart: options.restart }),
io,
});
if (!outcome.delegated) {
return undefined;
}
if (outcome.exitCode === 0) {
// Recorded here rather than in the hub-spawned process: this is the
// invocation that expressed the intent to keep the connector running.
persistConnectorConnection(connector.name, instanceId, passthroughArgs);
}
return outcome.exitCode;
}
export async function runConnectAdapter(
@@ -62,11 +383,20 @@ export async function runConnectAdapter(
io: ConnectIo,
): Promise<number> {
const connector = await getConnector(adapterName);
if (!connector) {
io.writeErr(`unknown connect adapter "${adapterName}"`);
return 1;
if (connector) {
const delegated = await tryDelegateToHub(connector, passthroughArgs, io);
if (delegated !== undefined) {
return delegated;
}
}
return connector.run(passthroughArgs, io);
const result = await runConnectAdapterWithResult(
adapterName,
passthroughArgs,
io,
);
return result.exitCode === CONNECT_ALREADY_RUNNING_EXIT_CODE
? 0
: result.exitCode;
}
export function formatAdapterList(): string {
+1 -1
View File
@@ -2,8 +2,8 @@ import { existsSync } from "node:fs";
import { arch, platform } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import open from "open";
import { configureSandboxEnvironment } from "../utils/helpers";
import open from "../utils/open";
import { c } from "../utils/output";
export interface DashboardServerHandle {
+216 -1
View File
@@ -9,6 +9,7 @@ import {
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { version as cliVersion } from "../../package.json";
import { getCliBuildInfo } from "../utils/common";
const {
@@ -21,7 +22,9 @@ const {
mockClearHubDiscovery,
mockStopLocalHubServerGracefully,
mockEnsureFileExists,
mockListActiveConnectors,
mockStopAllConnectors,
mockListSupervisedConnectors,
} = vi.hoisted(() => ({
mockSpawnSync: vi.fn(),
mockResolveClineDataDir: vi.fn(() => "/tmp/cline-data"),
@@ -49,11 +52,14 @@ const {
mockClearHubDiscovery: vi.fn(),
mockStopLocalHubServerGracefully: vi.fn(async () => false),
mockEnsureFileExists: vi.fn(),
mockListActiveConnectors: vi.fn(() => []),
mockStopAllConnectors: vi.fn(async () => ({
stoppedProcesses: 0,
failedProcesses: 0,
stoppedSessions: 0,
executed: 0,
})),
mockListSupervisedConnectors: vi.fn(async () => undefined as unknown),
}));
vi.mock("node:child_process", () => ({
@@ -69,6 +75,7 @@ vi.mock("@cline/core", () => ({
readHubDiscovery: mockReadHubDiscovery,
stopLocalHubServerGracefully: mockStopLocalHubServerGracefully,
ensureFileExists: mockEnsureFileExists,
listActiveConnectors: mockListActiveConnectors,
}));
vi.mock("../connectors/common", () => ({
@@ -79,7 +86,11 @@ vi.mock("./connect", () => ({
stopAllConnectors: mockStopAllConnectors,
}));
import { createDoctorCommand, runDoctorCommand } from "./doctor";
vi.mock("./connect-via-hub", () => ({
listSupervisedConnectorsViaHub: mockListSupervisedConnectors,
}));
import { __test__, createDoctorCommand, runDoctorCommand } from "./doctor";
describe("runDoctorCommand", () => {
const tempDirs: string[] = [];
@@ -99,6 +110,7 @@ describe("runDoctorCommand", () => {
mockStopLocalHubServerGracefully.mockResolvedValue(false);
mockStopAllConnectors.mockResolvedValue({
stoppedProcesses: 0,
failedProcesses: 0,
stoppedSessions: 0,
executed: 0,
});
@@ -174,6 +186,40 @@ describe("runDoctorCommand", () => {
);
});
it("reports CLI and running hub Core versions", async () => {
const cwd = "/workspace";
mockReadHubDiscovery.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
port: 25463,
pid: 50174,
coreVersion: "0.0.63",
});
mockProbeHubServer.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
port: 25463,
pid: 50174,
coreVersion: "0.0.64",
});
mockSpawnSync.mockReturnValue({ status: 1, stdout: "" });
const output: string[] = [];
const code = await runDoctorCommand(
{ cwd, json: true },
{
writeln: (text) => {
output.push(text ?? "");
},
writeErr: () => {},
},
);
expect(code).toBe(0);
expect(JSON.parse(output[0] || "")).toMatchObject({
cliVersion,
coreVersion: "0.0.64",
});
});
it("doctor --fix clears wedged hub startup artifacts when no server is actually running", async () => {
const cwd = mkdtempSync(path.join(os.tmpdir(), "doctor-hub-fix-"));
tempDirs.push(cwd);
@@ -248,6 +294,7 @@ describe("runDoctorCommand", () => {
mockSpawnSync.mockReturnValue({ status: 1, stdout: "" });
mockStopAllConnectors.mockResolvedValue({
stoppedProcesses: 2,
failedProcesses: 0,
stoppedSessions: 5,
executed: 3,
});
@@ -409,3 +456,171 @@ describe("createDoctorCommand log subcommand", () => {
expect(errors[0]).toContain("open failed");
});
});
describe("container-aware process filtering", () => {
const { decideForeignContainer, CONTAINER_CGROUP_PATTERN } = __test__;
it("treats a process in a different pid namespace as foreign", () => {
expect(
decideForeignContainer({
platform: "linux",
namespacePairs: [
["pid:[4026531836]", "pid:[4026532500]"],
[undefined, undefined],
],
ownContainerId: undefined,
otherContainerId: undefined,
}),
).toBe(true);
});
it("keeps a sibling process in our own namespaces", () => {
expect(
decideForeignContainer({
platform: "linux",
namespacePairs: [
["pid:[4026531836]", "pid:[4026531836]"],
["mnt:[4026531840]", "mnt:[4026531840]"],
],
ownContainerId: undefined,
otherContainerId: undefined,
}),
).toBe(false);
});
it("falls back to cgroup container ids when namespaces are unreadable", () => {
expect(
decideForeignContainer({
platform: "linux",
namespacePairs: [[undefined, undefined]],
ownContainerId: undefined,
otherContainerId: "7c6ffadc42f0bc0bc7c6ca47de4cd702",
}),
).toBe(true);
// Same container: our own sibling process, not something to retire.
expect(
decideForeignContainer({
platform: "linux",
namespacePairs: [[undefined, undefined]],
ownContainerId: "7c6ffadc42f0bc0bc7c6ca47de4cd702",
otherContainerId: "7c6ffadc42f0bc0bc7c6ca47de4cd702",
}),
).toBe(false);
});
it("never filters off Linux, where containers cannot share our pid space", () => {
expect(
decideForeignContainer({
platform: "darwin",
namespacePairs: [["pid:[1]", "pid:[2]"]],
ownContainerId: undefined,
otherContainerId: "abcdef123456",
}),
).toBe(false);
});
it("extracts container ids from real cgroup paths", () => {
const docker =
"0::/system.slice/docker-7c6ffadc42f0bc0bc7c6ca47de4cd702206e79b4068d172d8c2a2350063913ad.scope";
expect(docker.match(CONTAINER_CGROUP_PATTERN)?.[1]).toBe(
"7c6ffadc42f0bc0bc7c6ca47de4cd702206e79b4068d172d8c2a2350063913ad",
);
// A plain host session must not look like a container.
expect(
"0::/user.slice/user-1001.slice/session-121.scope".match(
CONTAINER_CGROUP_PATTERN,
),
).toBeNull();
});
});
describe("doctor supervision reporting", () => {
const { formatSupervisedConnector } = __test__;
afterEach(() => {
vi.clearAllMocks();
mockListSupervisedConnectors.mockResolvedValue(undefined);
});
async function runDoctorJson(): Promise<Record<string, unknown>> {
const output: string[] = [];
await runDoctorCommand(
{ cwd: "/workspace", json: true },
{
writeln: (text) => {
output.push(text ?? "");
},
writeErr: () => {},
},
);
return JSON.parse(output[0] || "{}") as Record<string, unknown>;
}
it("reports what the hub is supervising", async () => {
mockListSupervisedConnectors.mockResolvedValue([
{
channel: "slack",
instanceId: "cline-slack",
state: "backoff",
origin: "spawned",
restarts: 3,
},
]);
await expect(runDoctorJson()).resolves.toMatchObject({
supervisedConnectors: [
{ channel: "slack", instanceId: "cline-slack", state: "backoff" },
],
});
});
it("omits supervision when the hub cannot report it", async () => {
mockListSupervisedConnectors.mockResolvedValue(undefined);
const status = await runDoctorJson();
expect(status.supervisedConnectors).toBeUndefined();
});
it("stays usable when the supervision query fails", async () => {
mockListSupervisedConnectors.mockRejectedValue(new Error("hub gone"));
// Diagnostics must degrade quietly rather than fail.
const status = await runDoctorJson();
expect(status.supervisedConnectors).toBeUndefined();
expect(status).toHaveProperty("hubHealthy");
});
it("formats restart and failure state so a crash loop is visible", () => {
expect(
formatSupervisedConnector({
channel: "slack",
instanceId: "cline-slack",
state: "failed",
origin: "adopted",
pid: 42,
restarts: 5,
lastExitCode: 1,
lastError: "invalid token",
}),
).toBe(
"slack | instance=cline-slack | state=failed | origin=adopted | pid=42 | restarts=5 | lastExit=1 | error=invalid token",
);
});
it("leaves out fields that do not apply to a healthy connector", () => {
expect(
formatSupervisedConnector({
channel: "telegram",
instanceId: "cline_bot",
state: "running",
origin: "spawned",
pid: 7,
restarts: 0,
}),
).toBe(
"telegram | instance=cline_bot | state=running | origin=spawned | pid=7",
);
});
});
+145 -8
View File
@@ -1,9 +1,10 @@
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, rmSync } from "node:fs";
import { existsSync, readFileSync, readlinkSync, rmSync } from "node:fs";
import { dirname, join } from "node:path";
import {
clearHubDiscovery,
ensureFileExists,
listActiveConnectors,
probeHubServer,
readHubDiscovery,
resolveClineDataDir,
@@ -11,17 +12,20 @@ import {
resolveSharedHubOwnerContext,
stopLocalHubServerGracefully,
} from "@cline/core";
import { formatUptime, resolveClineBuildEnv } from "@cline/shared";
import { Command } from "commander";
import open from "open";
import { isProcessRunning } from "../connectors/common";
import {
type ActiveConnectorRecord,
listActiveConnectors,
} from "../connectors/status";
formatUptime,
resolveClineBuildEnv,
type SupervisedConnectorRecord,
} from "@cline/shared";
import { Command } from "commander";
import { version as cliVersion } from "../../package.json";
import { isProcessRunning } from "../connectors/common";
import { getCliBuildInfo } from "../utils/common";
import open from "../utils/open";
import { c, writeln } from "../utils/output";
import { stopAllConnectors } from "./connect";
import { listSupervisedConnectorsViaHub } from "./connect-via-hub";
type DoctorIo = {
writeln: (text?: string) => void;
@@ -49,6 +53,8 @@ type SpawnedProcessRecord = {
type DoctorStatus = {
cwd: string;
cliVersion: string;
coreVersion?: string;
hubUrl?: string;
hubHealthy: boolean;
hubPid?: number;
@@ -60,6 +66,8 @@ type DoctorStatus = {
staleCliPids: number[];
staleSidecarPids: number[];
activeConnectors: ActiveConnectorRecord[];
/** Undefined when the running hub cannot report supervision. */
supervisedConnectors?: SupervisedConnectorRecord[];
recentSpawnedProcesses: SpawnedProcessRecord[];
};
@@ -68,6 +76,13 @@ type ProcessRecord = {
command: string;
};
// Container id inside a cgroup path, e.g.
// "0::/system.slice/docker-<64-hex>.scope" (docker/containerd/podman) or
// "/kubepods/.../<64-hex>" (kubernetes). Captures the id so two different
// containers can be told apart, not merely "is containerised".
const CONTAINER_CGROUP_PATTERN =
/(?:docker[-/]|containerd[-/]|libpod[-/]|crio[-/]|lxc[-/.])([0-9a-f]{12,64})/;
function parsePids(raw: string): number[] {
return raw
.split(/\r?\n/)
@@ -75,6 +90,77 @@ function parsePids(raw: string): number[] {
.filter((pid) => Number.isInteger(pid) && pid > 0);
}
function tryReadLink(target: string): string | undefined {
try {
return readlinkSync(target);
} catch {
return undefined;
}
}
function readContainerCgroupId(pid: number | "self"): string | undefined {
let raw: string;
try {
raw = readFileSync(`/proc/${pid}/cgroup`, "utf8");
} catch {
return undefined;
}
return raw.match(CONTAINER_CGROUP_PATTERN)?.[1];
}
/**
* Decide whether a process belongs to a container other than our own.
*
* Namespace identity is the reliable signal: a containerised process has
* different PID/mount namespaces than the host process running the scan.
* Container ids parsed from cgroup paths are the fallback for kernels where the
* namespace links are unreadable. Unknown on both sides means "assume ours",
* preserving the previous behaviour rather than silently dropping processes the
* user does want cleaned up.
*/
function decideForeignContainer(input: {
platform: string;
namespacePairs: Array<[string | undefined, string | undefined]>;
ownContainerId: string | undefined;
otherContainerId: string | undefined;
}): boolean {
// /proc/<pid>/ns exists only on Linux. Elsewhere containers run inside a VM
// and never share a pid space with us, so there is nothing to disambiguate.
if (input.platform !== "linux") {
return false;
}
for (const [own, other] of input.namespacePairs) {
if (own && other && own !== other) {
return true;
}
}
return (
Boolean(input.otherContainerId) &&
input.otherContainerId !== input.ownContainerId
);
}
/**
* True when `pid` belongs to a container other than this process's own.
*
* `pgrep` sees every process on the host, containers included: a Docker agent's
* hub daemon shows up beside ours, and when the container shares our uid `kill`
* on it succeeds. Those daemons are emphatically not stale — they belong to a
* live agent with its own data dir — so reporting them, and killing them in
* `doctor fix`, takes down an unrelated agent.
*/
function isForeignContainerPid(pid: number): boolean {
return decideForeignContainer({
platform: process.platform,
namespacePairs: (["pid", "mnt"] as const).map((namespace) => [
tryReadLink(`/proc/self/ns/${namespace}`),
tryReadLink(`/proc/${pid}/ns/${namespace}`),
]),
ownContainerId: readContainerCgroupId("self"),
otherContainerId: readContainerCgroupId(pid),
});
}
function listMatchingProcesses(pattern: string): ProcessRecord[] {
if (process.platform === "win32") {
return [];
@@ -104,7 +190,8 @@ function listMatchingProcesses(pattern: string): ProcessRecord[] {
pid <= 0 ||
!command ||
pid === process.pid ||
pid === process.ppid
pid === process.ppid ||
isForeignContainerPid(pid)
) {
continue;
}
@@ -337,6 +424,8 @@ async function collectDoctorStatus(cwd: string): Promise<DoctorStatus> {
];
return {
cwd,
cliVersion,
coreVersion: health?.coreVersion ?? discovery?.coreVersion,
hubUrl: current?.url,
hubHealthy: !!health?.url,
hubPid: current?.pid,
@@ -348,6 +437,7 @@ async function collectDoctorStatus(cwd: string): Promise<DoctorStatus> {
staleCliPids: listStaleCliPids(),
staleSidecarPids: listStaleSidecarPids(),
activeConnectors: listActiveConnectors(),
...((await listSupervisedConnectorsSafely()) ?? {}),
recentSpawnedProcesses: readRecentSpawnedProcesses(),
};
}
@@ -372,6 +462,39 @@ function formatRecentSpawnedProcess(record: SpawnedProcessRecord): string {
return pieces.join(" | ");
}
/**
* Supervision is reported by the running hub, so it is unavailable whenever
* there is no hub or it predates supervision. Diagnostics must degrade quietly
* rather than fail.
*/
async function listSupervisedConnectorsSafely(): Promise<
{ supervisedConnectors: SupervisedConnectorRecord[] } | undefined
> {
try {
const supervised = await listSupervisedConnectorsViaHub();
return supervised ? { supervisedConnectors: supervised } : undefined;
} catch {
return undefined;
}
}
function formatSupervisedConnector(record: SupervisedConnectorRecord): string {
const pieces = [
record.channel,
`instance=${record.instanceId}`,
`state=${record.state}`,
`origin=${record.origin}`,
record.pid === undefined ? undefined : `pid=${record.pid}`,
record.restarts > 0 ? `restarts=${record.restarts}` : undefined,
record.nextRestartAt ? `nextRestart=${record.nextRestartAt}` : undefined,
record.lastExitCode === undefined
? undefined
: `lastExit=${record.lastExitCode}`,
record.lastError ? `error=${record.lastError}` : undefined,
];
return pieces.filter(Boolean).join(" | ");
}
function formatActiveConnector(record: ActiveConnectorRecord): string {
const identity =
record.type === "telegram"
@@ -405,6 +528,12 @@ function killPids(pids: number[]): number {
return killed;
}
export const __test__ = {
decideForeignContainer,
CONTAINER_CGROUP_PATTERN,
formatSupervisedConnector,
};
export async function runDoctorCommand(
opts: { cwd: string; json?: boolean; fix?: boolean; verbose?: boolean },
io: DoctorIo,
@@ -419,6 +548,8 @@ export async function runDoctorCommand(
io.writeln(JSON.stringify(before));
return 0;
}
writeln(`cli version ${c.dim}${before.cliVersion}${c.reset}`);
writeln(`core version ${c.dim}${before.coreVersion ?? "n/a"}${c.reset}`);
writeln(`hub url ${c.dim}${before.hubUrl ?? "none"}${c.reset}`);
writeln(
`hub healthy ${c.dim}${before.hubHealthy ? "yes" : "no"}${before.hubPid ? ` (pid=${before.hubPid})` : ""}${c.reset}`,
@@ -442,6 +573,12 @@ export async function runDoctorCommand(
writeln(`- ${c.dim}${formatActiveConnector(record)}${c.reset}`);
}
}
if (before.supervisedConnectors?.length) {
writeln("hub-supervised connectors:");
for (const record of before.supervisedConnectors) {
writeln(`- ${c.dim}${formatSupervisedConnector(record)}${c.reset}`);
}
}
if (verbose && before.recentSpawnedProcesses.length > 0) {
writeln("recent spawned processes:");
for (const record of before.recentSpawnedProcesses) {
@@ -0,0 +1,94 @@
import { Command } from "commander";
import { beforeEach, describe, expect, it, vi } from "vitest";
const historyMocks = vi.hoisted(() => ({
runHistoryDelete: vi.fn(async () => 0),
runHistoryExport: vi.fn(async () => 0),
runHistoryList: vi.fn(async () => 0),
runHistoryUpdate: vi.fn(async () => 0),
}));
vi.mock("./history", () => historyMocks);
import { registerHistoryCommand } from "./history-command";
function createHarness(isInteractiveTTY: boolean) {
const program = new Command()
.exitOverride()
.option("--json", "Output as JSON");
program.configureOutput({
writeOut: vi.fn(),
writeErr: vi.fn(),
});
const io = {
writeln: vi.fn(),
writeErr: vi.fn(),
};
const setExitCode = vi.fn();
const setStartupTarget = vi.fn();
registerHistoryCommand({
program,
io,
setExitCode,
setStartupTarget,
isInteractiveTTY: () => isInteractiveTTY,
});
return { program, io, setExitCode, setStartupTarget };
}
describe("registerHistoryCommand", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("opens the in-app history picker for an interactive text terminal", async () => {
const { program, setExitCode, setStartupTarget } = createHarness(true);
await program.parseAsync(["history"], { from: "user" });
expect(setStartupTarget).toHaveBeenCalledOnce();
expect(setStartupTarget).toHaveBeenCalledWith("history");
expect(historyMocks.runHistoryList).not.toHaveBeenCalled();
expect(setExitCode).not.toHaveBeenCalled();
});
it("keeps explicit JSON output non-interactive even when a TTY is attached", async () => {
const { program, io, setExitCode, setStartupTarget } = createHarness(true);
await program.parseAsync(["history", "--json"], { from: "user" });
expect(setStartupTarget).not.toHaveBeenCalled();
expect(historyMocks.runHistoryList).toHaveBeenCalledWith({
limit: 50,
outputMode: "json",
io,
});
expect(setExitCode).toHaveBeenCalledWith(0);
});
it("prints text history when no interactive terminal is attached", async () => {
const { program, io, setExitCode, setStartupTarget } = createHarness(false);
await program.parseAsync(["history", "--limit", "12"], { from: "user" });
expect(setStartupTarget).not.toHaveBeenCalled();
expect(historyMocks.runHistoryList).toHaveBeenCalledWith({
limit: 12,
outputMode: "text",
io,
});
expect(setExitCode).toHaveBeenCalledWith(0);
});
it("returns an error when delete is missing --session-id", async () => {
const { program, io, setExitCode } = createHarness(false);
await program.parseAsync(["history", "delete"], { from: "user" });
expect(io.writeErr).toHaveBeenCalledWith(
"history delete requires --session-id <id>",
);
expect(historyMocks.runHistoryDelete).not.toHaveBeenCalled();
expect(setExitCode).toHaveBeenCalledWith(1);
});
});
+117
View File
@@ -0,0 +1,117 @@
import type { Command } from "commander";
import type { TuiStartupTarget } from "../tui/types";
import type { CliOutputMode } from "../utils/types";
import {
runHistoryDelete,
runHistoryExport,
runHistoryList,
runHistoryUpdate,
} from "./history";
type HistoryCommandIo = {
writeln: (text?: string) => void;
writeErr: (text: string) => void;
};
type RegisterHistoryCommandOptions = {
program: Command;
io: HistoryCommandIo;
setExitCode: (code: number) => void;
setStartupTarget: (target: TuiStartupTarget) => void;
isInteractiveTTY?: () => boolean;
};
function resolveHistoryOutputMode(
program: Command,
historyCmd: Command,
): CliOutputMode {
return program.opts().json || historyCmd.opts().json ? "json" : "text";
}
export function registerHistoryCommand({
program,
io,
setExitCode,
setStartupTarget,
isInteractiveTTY = () =>
process.stdin.isTTY === true && process.stdout.isTTY === true,
}: RegisterHistoryCommandOptions): void {
const historyCmd = program
.command("history")
.alias("h")
.description("List session history or manage saved sessions")
.option("--json", "Output as JSON")
.option("--limit <count>", "Maximum number of sessions to show", "50")
.option("--page <number>", "Page number for paginated results")
.option("--config <dir>", "configuration directory")
.action(async () => {
const opts = historyCmd.opts();
const limit = Number.parseInt(opts.limit, 10);
const outputMode = resolveHistoryOutputMode(program, historyCmd);
if (outputMode === "text" && isInteractiveTTY()) {
setStartupTarget("history");
return;
}
setExitCode(
await runHistoryList({
limit,
outputMode,
io,
}),
);
});
const historyDeleteCmd = historyCmd
.command("delete")
.description("Delete a session from history")
.option("--session-id <id>", "Session ID to delete")
.action(async () => {
const opts = historyDeleteCmd.opts();
if (!opts.sessionId) {
io.writeErr("history delete requires --session-id <id>");
setExitCode(1);
return;
}
const outputMode = resolveHistoryOutputMode(program, historyCmd);
setExitCode(await runHistoryDelete(opts.sessionId, outputMode, io));
});
const historyUpdateCmd = historyCmd
.command("update")
.description("Update a session in history")
.option("--metadata <json>", "Metadata as JSON string")
.option("--prompt <text>", "New prompt text")
.option("--session-id <id>", "Session ID to update")
.option("--title <text>", "New title")
.action(async () => {
const opts = historyUpdateCmd.opts();
if (!opts.sessionId) {
io.writeErr("history update requires --session-id <id>");
setExitCode(1);
return;
}
const outputMode = resolveHistoryOutputMode(program, historyCmd);
setExitCode(
await runHistoryUpdate(
opts.sessionId,
opts.prompt,
opts.title,
opts.metadata,
outputMode,
io,
),
);
});
const historyExportCmd = historyCmd
.command("export <sessionId>")
.description("Export a session as a standalone HTML file")
.option("-o, --output <path>", "Output HTML file path")
.action(async (sessionId: string) => {
const opts = historyExportCmd.opts();
const outputMode = resolveHistoryOutputMode(program, historyCmd);
setExitCode(
await runHistoryExport(sessionId, opts.output, outputMode, io),
);
});
}
+83 -10
View File
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import type { SessionHistoryRecord } from "@cline/core";
import { afterEach, describe, expect, it, vi } from "vitest";
import { exportHistorySession } from "../session/history-export";
import {
formatCheckpointDetail,
formatHistoryListLine,
@@ -16,18 +17,12 @@ vi.mock("../session/session", () => ({
readSessionMessagesArtifact: vi.fn(),
}));
vi.mock("../tui/history-standalone", () => ({
renderHistoryStandalone: vi.fn(async () => 0),
}));
import { listSessions, readSessionMessagesArtifact } from "../session/session";
import { renderHistoryStandalone } from "../tui/history-standalone";
const mockedReadSessionMessagesArtifact = vi.mocked(
readSessionMessagesArtifact,
);
const mockedListSessions = vi.mocked(listSessions);
const mockedRenderHistoryStandalone = vi.mocked(renderHistoryStandalone);
function createHistoryRow(
overrides: Partial<SessionHistoryRecord> = {},
@@ -200,8 +195,11 @@ describe("runHistoryList", () => {
vi.clearAllMocks();
});
it("hydrates interactive history rows so titles can be inferred from messages", async () => {
const row = createHistoryRow({ prompt: undefined, metadata: undefined });
it("requests hydrated text history rows so titles can come from messages", async () => {
const row = createHistoryRow({
prompt: undefined,
metadata: { title: "hydrated title", totalCost: 0.25 },
});
mockedListSessions.mockResolvedValue([row]);
const io = {
writeln: vi.fn(),
@@ -218,8 +216,8 @@ describe("runHistoryList", () => {
expect(mockedListSessions).toHaveBeenCalledWith(25, {
hydrate: true,
});
expect(mockedRenderHistoryStandalone).toHaveBeenCalledWith(
expect.objectContaining({ rows: [row] }),
expect(io.writeln).toHaveBeenCalledWith(
expect.stringContaining("hydrated title"),
);
});
@@ -313,6 +311,81 @@ describe("runHistoryExport", () => {
await expect(readFile(outputPath, "utf8")).resolves.toContain("world");
});
it("writes structured JSON from a persisted messages artifact", async () => {
tempDir = await mkdtemp(join(tmpdir(), "cline-history-export-"));
const artifact = {
version: 1,
updated_at: "2026-04-22T17:42:10.123Z",
sessionId: "sess_1",
systemPrompt: "Be helpful",
messages: [
{
id: "m1",
role: "user",
content: [{ type: "text", text: "hello" }],
},
{
id: "m2",
role: "assistant",
content: [{ type: "text", text: "world" }],
},
],
} satisfies NonNullable<
Awaited<ReturnType<typeof readSessionMessagesArtifact>>
>;
mockedReadSessionMessagesArtifact.mockResolvedValue(artifact);
const targetPath = await exportHistorySession({
sessionId: "sess_1",
format: "json",
outputDirectory: tempDir,
});
expect(targetPath).toBe(join(tempDir, "sess_1.json"));
await expect(
readFile(targetPath, "utf8").then((contents) => JSON.parse(contents)),
).resolves.toEqual(artifact);
});
it("exports run_commands history with structured command objects", async () => {
tempDir = await mkdtemp(join(tmpdir(), "cline-history-export-"));
const outputPath = join(tempDir, "export.html");
const artifact = {
version: 1,
updated_at: "2026-04-22T17:42:10.123Z",
sessionId: "sess_1",
messages: [
{
id: "m1",
role: "assistant",
content: [
{
type: "tool_use",
id: "tool_1",
name: "run_commands",
input: {
commands: [{ command: "cmd", args: ["/c", "dir"] }],
},
},
],
},
],
} satisfies NonNullable<
Awaited<ReturnType<typeof readSessionMessagesArtifact>>
>;
mockedReadSessionMessagesArtifact.mockResolvedValue(artifact);
const io = {
writeln: vi.fn(),
writeErr: vi.fn(),
};
const code = await runHistoryExport("sess_1", outputPath, "text", io);
expect(code).toBe(0);
expect(io.writeErr).not.toHaveBeenCalled();
await expect(readFile(outputPath, "utf8")).resolves.toContain("cmd /c dir");
});
it("fails when the session artifact is missing", async () => {
mockedReadSessionMessagesArtifact.mockResolvedValue(undefined);
const io = {
+14 -46
View File
@@ -1,13 +1,6 @@
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { generateConversationHTML } from "../session/export";
import {
deleteSession,
listSessions,
readSessionMessagesArtifact,
updateSession,
} from "../session/session";
import { disableOpenTuiGraphicsProbe } from "../tui/opentui-env";
import { exportHistorySession } from "../session/history-export";
import { deleteSession, listSessions, updateSession } from "../session/session";
import { formatHistoryListLine } from "../utils/history-format";
import { writeln } from "../utils/output";
import type { CliOutputMode } from "../utils/types";
@@ -22,22 +15,6 @@ type HistoryIo = {
writeErr: (text: string) => void;
};
async function exportHistorySession(
sessionId: string,
outputPath?: string,
): Promise<string> {
const data = await readSessionMessagesArtifact(sessionId);
if (!data) {
throw new Error(`Session ${sessionId} not found or has no messages.json`);
}
const targetPath = resolve(outputPath?.trim() || `${sessionId}.html`);
const html = generateConversationHTML(data, sessionId);
await mkdir(dirname(targetPath), { recursive: true });
await writeFile(targetPath, html, "utf8");
return targetPath;
}
async function runHistoryDelete(
sessionId: string | undefined,
outputMode: CliOutputMode,
@@ -136,7 +113,11 @@ async function runHistoryExport(
}
try {
const targetPath = await exportHistorySession(sessionId, outputPath);
const targetPath = await exportHistorySession({
sessionId,
format: "html",
outputPath,
});
if (outputMode === "json") {
process.stdout.write(
@@ -161,7 +142,7 @@ export async function runHistoryList(input: {
outputMode: CliOutputMode;
workspaceRoot?: string;
io?: HistoryIo;
}): Promise<number | string> {
}): Promise<number> {
const io = input.io ?? {
writeln,
writeErr: (text: string) => process.stderr.write(`${text}\n`),
@@ -186,23 +167,10 @@ export async function runHistoryList(input: {
return 0;
}
disableOpenTuiGraphicsProbe();
const { renderHistoryStandalone } = await import("../tui/history-standalone");
return await renderHistoryStandalone({
rows,
refreshRows: async () =>
await listSessions(limit, {
workspaceRoot: input.workspaceRoot,
hydrate: false,
}),
onExport: async (sessionId: string) =>
await exportHistorySession(sessionId, undefined),
});
for (const row of rows) {
io.writeln(formatHistoryListLine(row));
}
return 0;
}
export {
exportHistorySession,
runHistoryDelete,
runHistoryExport,
runHistoryUpdate,
};
export { runHistoryDelete, runHistoryExport, runHistoryUpdate };
+4
View File
@@ -34,6 +34,7 @@ vi.mock("@cline/core", () => ({
stopLocalHubServerGracefully: mockStopLocalHubServerGracefully,
}));
import { version as cliVersion } from "../../package.json";
import { createHubCommand } from "./hub";
const originalBuildEnv = process.env.CLINE_BUILD_ENV;
@@ -63,6 +64,7 @@ describe("createHubCommand", () => {
port: 25463,
pid: 50174,
startedAt: "2026-01-01T00:00:00.000Z",
coreVersion: "0.0.62",
});
const output: string[] = [];
@@ -88,6 +90,8 @@ describe("createHubCommand", () => {
pid: 50174,
startedAt: "2026-01-01T00:00:00.000Z",
uptime: "1m 5s",
cliVersion,
coreVersion: "0.0.62",
});
});
+3
View File
@@ -9,6 +9,7 @@ import {
} from "@cline/core";
import { formatUptime, resolveClineBuildEnv } from "@cline/shared";
import { Command } from "commander";
import { version as cliVersion } from "../../package.json";
interface HubCommandIo {
writeln: (text?: string) => void;
@@ -134,6 +135,8 @@ export function createHubCommand(
pid: health?.pid,
startedAt: health?.startedAt,
uptime,
cliVersion,
coreVersion: health?.coreVersion ?? discovery?.coreVersion,
}),
);
}),
+276 -5
View File
@@ -1,5 +1,31 @@
import { describe, expect, it, vi } from "vitest";
import { buildMcpInstallDefaults, runMcpInstallCommand } from "./mcp";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { installMcpServer } from "@cline/core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
buildMcpInstallDefaults,
buildMcpInstallTransport,
runMcpInstallCommand,
runMcpUninstallCommand,
} from "./mcp";
vi.mock("@cline/core", async (importOriginal) => {
const actual = await importOriginal<typeof import("@cline/core")>();
return {
...actual,
installMcpServer: vi.fn((options) => {
const { name, transport, warnings } =
actual.buildMcpInstallTransport(options);
return {
name,
status: "installed",
transport,
warnings,
};
}),
};
});
describe("mcp install command", () => {
it("builds stdio wizard defaults from command args", () => {
@@ -34,6 +60,19 @@ describe("mcp install command", () => {
});
});
it("shows mcp-remote marketplace entries as native remote servers", () => {
expect(
buildMcpInstallDefaults({
name: "linear",
targetArgs: ["npx", "-y", "mcp-remote", "https://mcp.linear.app/mcp"],
}),
).toEqual({
name: "linear",
type: "streamableHttp",
url: "https://mcp.linear.app/mcp",
});
});
it("normalizes streamable-http transport", () => {
expect(
buildMcpInstallDefaults({
@@ -88,6 +127,52 @@ describe("mcp install command", () => {
).toThrow(/only http and https are supported/);
});
it("builds direct stdio installs without shell-joining args", () => {
expect(
buildMcpInstallTransport({
name: "fs",
targetArgs: [
"npx",
"-y",
"@modelcontextprotocol/server-filesystem",
"/tmp/my dir",
],
}),
).toEqual({
name: "fs",
transport: {
type: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp/my dir"],
},
warnings: [],
});
});
it("builds direct remote installs with headers and placeholder warnings", () => {
expect(
buildMcpInstallTransport({
name: "docs",
transport: "http",
headers: ["Authorization: Bearer <token>"],
targetArgs: ["https://example.com/mcp", "--header=X-Extra: yes"],
}),
).toEqual({
name: "docs",
transport: {
type: "streamableHttp",
url: "https://example.com/mcp",
headers: {
Authorization: "Bearer <token>",
"X-Extra": "yes",
},
},
warnings: [
'Header "Authorization" looks like it contains a placeholder. Update it in MCP settings before using this server.',
],
});
});
it("opens the add wizard with prefilled defaults", async () => {
const runWizard = vi.fn(async () => 0);
@@ -124,11 +209,11 @@ describe("mcp install command", () => {
expect(code).toBe(1);
expect(runWizard).not.toHaveBeenCalled();
expect(writeErr).toHaveBeenCalledWith(
"cline mcp install opens the MCP wizard and requires a TTY.",
"cline mcp install opens the MCP wizard and requires a TTY. Pass --yes to install noninteractively.",
);
});
it("checks for TTY before validating install arguments", async () => {
it("checks for TTY before validating wizard install arguments", async () => {
const writeErr = vi.fn();
const code = await runMcpInstallCommand({
@@ -139,7 +224,193 @@ describe("mcp install command", () => {
expect(code).toBe(1);
expect(writeErr).toHaveBeenCalledWith(
"cline mcp install opens the MCP wizard and requires a TTY.",
"cline mcp install opens the MCP wizard and requires a TTY. Pass --yes to install noninteractively.",
);
});
it("installs directly with --yes without requiring a TTY", async () => {
const writeln = vi.fn();
const writeErr = vi.fn();
const code = await runMcpInstallCommand({
name: "docs",
transport: "http",
targetArgs: [
"https://example.com/mcp",
"--header",
"Authorization: Bearer token",
],
isTty: false,
yes: true,
io: { writeln, writeErr },
});
expect(code).toBe(0);
expect(installMcpServer).toHaveBeenCalledWith({
name: "docs",
transport: "http",
targetArgs: [
"https://example.com/mcp",
"--header",
"Authorization: Bearer token",
],
isTty: false,
yes: true,
io: { writeln, writeErr },
});
expect(writeln).toHaveBeenCalledWith("Installed MCP server docs.");
expect(writeErr).not.toHaveBeenCalled();
});
it("prints direct install JSON with --yes --json", async () => {
const writeln = vi.fn();
const code = await runMcpInstallCommand({
name: "fs",
targetArgs: ["node", "server.js"],
isTty: false,
yes: true,
json: true,
io: { writeln, writeErr: vi.fn() },
});
expect(code).toBe(0);
expect(JSON.parse(writeln.mock.calls[0]?.[0])).toMatchObject({
name: "fs",
status: "installed",
transport: {
type: "stdio",
command: "node",
args: ["server.js"],
},
});
});
});
describe("mcp uninstall command", () => {
let root = "";
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "cli-mcp-uninstall-"));
});
afterEach(() => {
rmSync(root, { recursive: true, force: true });
});
function writeSettings(): string {
const settingsPath = join(root, "cline_mcp_settings.json");
writeFileSync(
settingsPath,
JSON.stringify(
{
mcpServers: {
docs: {
transport: {
type: "streamableHttp",
url: "https://example.com/mcp",
},
},
keep: {
transport: { type: "stdio", command: "node" },
disabled: true,
},
},
customTopLevelKey: true,
},
null,
2,
),
"utf8",
);
return settingsPath;
}
function readSettings(settingsPath: string): Record<string, unknown> & {
mcpServers?: Record<string, unknown>;
} {
return JSON.parse(readFileSync(settingsPath, "utf8")) as Record<
string,
unknown
> & { mcpServers?: Record<string, unknown> };
}
it("uninstalls the requested server and reports success", async () => {
const settingsPath = writeSettings();
const writeln = vi.fn();
const writeErr = vi.fn();
const code = await runMcpUninstallCommand({
name: "docs",
settingsPath,
io: { writeln, writeErr },
});
expect(code).toBe(0);
expect(writeln).toHaveBeenCalledWith("Uninstalled MCP server docs.");
expect(writeErr).not.toHaveBeenCalled();
const written = readSettings(settingsPath);
expect(Object.keys(written.mcpServers ?? {})).toEqual(["keep"]);
expect(written.mcpServers?.keep).toEqual({
transport: { type: "stdio", command: "node" },
disabled: true,
});
expect(written.customTopLevelKey).toBe(true);
});
it("prints uninstall JSON with --json", async () => {
const settingsPath = writeSettings();
const writeln = vi.fn();
const code = await runMcpUninstallCommand({
name: "docs",
settingsPath,
json: true,
io: { writeln, writeErr: vi.fn() },
});
expect(code).toBe(0);
expect(JSON.parse(writeln.mock.calls[0]?.[0])).toEqual({
name: "docs",
status: "uninstalled",
});
expect(writeln).toHaveBeenCalledTimes(1);
});
it("reports an error and leaves settings intact for an unknown server", async () => {
const settingsPath = writeSettings();
const before = readFileSync(settingsPath, "utf8");
const writeln = vi.fn();
const writeErr = vi.fn();
const code = await runMcpUninstallCommand({
name: "missing",
settingsPath,
io: { writeln, writeErr },
});
expect(code).toBe(1);
expect(writeErr).toHaveBeenCalledWith(
'MCP server "missing" is not installed.',
);
expect(writeln).not.toHaveBeenCalled();
expect(readFileSync(settingsPath, "utf8")).toBe(before);
});
it("rejects a blank name without rewriting settings", async () => {
const settingsPath = writeSettings();
const before = readFileSync(settingsPath, "utf8");
const writeErr = vi.fn();
const code = await runMcpUninstallCommand({
name: " ",
settingsPath,
io: { writeErr },
});
expect(code).toBe(1);
expect(writeErr).toHaveBeenCalledWith("MCP server name is required");
expect(readFileSync(settingsPath, "utf8")).toBe(before);
});
});
+93 -60
View File
@@ -1,49 +1,35 @@
import {
buildMcpInstallTransport as buildCoreMcpInstallTransport,
type McpInstallOptions as CoreMcpInstallOptions,
installMcpServer,
type McpInstallResult,
type McpServerTransportConfig,
type McpUninstallOptions as CoreMcpUninstallOptions,
type McpUninstallResult as CoreMcpUninstallResult,
uninstallMcpServer,
} from "@cline/core";
import type { McpAddDefaults } from "../wizards/mcp";
export { buildMcpInstallTransport, uninstallMcpServer } from "@cline/core";
export interface McpCommandIo {
writeln?: (text: string) => void;
writeErr: (text: string) => void;
}
export interface McpInstallOptions {
name: string;
targetArgs?: string[];
transport?: string;
export interface McpInstallOptions extends CoreMcpInstallOptions {
io?: McpCommandIo;
isTty?: boolean;
json?: boolean;
runWizard?: (defaults: McpAddDefaults) => Promise<number>;
yes?: boolean;
}
function normalizeTransportType(
value: string | undefined,
): McpAddDefaults["type"] {
const normalized = (value ?? "stdio").trim();
if (normalized === "http" || normalized === "streamable-http") {
return "streamableHttp";
}
if (
normalized === "stdio" ||
normalized === "sse" ||
normalized === "streamableHttp"
) {
return normalized;
}
throw new Error(
`Unsupported MCP transport "${normalized}". Expected stdio, sse, http, streamable-http, or streamableHttp.`,
);
}
function assertValidUrl(url: string): void {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
throw new Error(`Invalid MCP server URL: ${url}`);
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error(
`Invalid MCP server URL: ${url} (only http and https are supported)`,
);
}
export interface McpInstallDirectResult {
name: string;
status: "installed";
transport: McpServerTransportConfig;
warnings: string[];
}
function quoteCommandArg(arg: string): string {
@@ -58,36 +44,32 @@ export function buildMcpInstallDefaults(options: {
targetArgs?: string[];
transport?: string;
}): McpAddDefaults {
const name = options.name.trim();
if (!name) {
throw new Error("MCP server name is required");
}
const type = normalizeTransportType(options.transport);
const targetArgs = options.targetArgs ?? [];
if (type === "stdio") {
if (targetArgs.length === 0) {
throw new Error(
"Stdio MCP install requires a command after the server name, for example: cline mcp install fs -- npx -y @modelcontextprotocol/server-filesystem /tmp",
);
}
const { name, transport } = buildCoreMcpInstallTransport(options);
if (transport.type === "stdio") {
return {
name,
type,
command: targetArgs.map(quoteCommandArg).join(" "),
type: transport.type,
command: [transport.command, ...(transport.args ?? [])]
.map(quoteCommandArg)
.join(" "),
};
}
if (targetArgs.length !== 1) {
throw new Error(
"Remote MCP install requires exactly one URL argument after the server name.",
);
}
const url = targetArgs[0]?.trim() ?? "";
assertValidUrl(url);
return {
name,
type,
url,
type: transport.type,
url: transport.url,
};
}
export function installMcpServerDirect(
options: McpInstallOptions,
): McpInstallDirectResult {
const result: McpInstallResult = installMcpServer(options);
return {
name: result.name,
status: result.status,
transport: result.transport,
warnings: result.warnings,
};
}
@@ -104,11 +86,23 @@ export async function runMcpInstallCommand(
options: McpInstallOptions,
): Promise<number> {
try {
if (options.yes) {
const result = installMcpServerDirect(options);
if (options.json) {
options.io?.writeln?.(JSON.stringify(result));
} else {
options.io?.writeln?.(`Installed MCP server ${result.name}.`);
for (const warning of result.warnings) {
options.io?.writeErr(warning);
}
}
return 0;
}
const isTty =
options.isTty ?? (process.stdin.isTTY && process.stdout.isTTY);
if (!isTty) {
throw new Error(
"cline mcp install opens the MCP wizard and requires a TTY.",
"cline mcp install opens the MCP wizard and requires a TTY. Pass --yes to install noninteractively.",
);
}
const defaults = buildMcpInstallDefaults(options);
@@ -119,3 +113,42 @@ export async function runMcpInstallCommand(
return 1;
}
}
export interface McpUninstallOptions extends CoreMcpUninstallOptions {
io?: McpCommandIo;
json?: boolean;
}
export interface McpUninstallDirectResult extends CoreMcpUninstallResult {}
export function uninstallMcpServerDirect(
options: McpUninstallOptions,
): McpUninstallDirectResult {
const result: CoreMcpUninstallResult = uninstallMcpServer(options);
return {
name: result.name,
status: result.status,
};
}
export async function runMcpUninstallCommand(
options: McpUninstallOptions,
): Promise<number> {
try {
const name = options.name?.trim() ?? "";
if (!name) {
throw new Error("MCP server name is required");
}
const result = uninstallMcpServerDirect({ ...options, name });
if (options.json) {
options.io?.writeln?.(JSON.stringify(result));
} else {
options.io?.writeln?.(`Uninstalled MCP server ${result.name}.`);
}
return 0;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
options.io?.writeErr(message);
return 1;
}
}
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
import { relative, sep } from "node:path";
import {
resolveClineDataDir,
resolveClineDir,
setHomeDir,
} from "@cline/shared/storage";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createProgram } from "./program";
/** Render an absolute path under `home` the way help text does: `~/...`. */
function tildePath(absolutePath: string, home: string): string {
return `~/${relative(home, absolutePath).split(sep).join("/")}`;
}
describe("root option help text", () => {
const FAKE_HOME = "/home/cline-help-test";
const savedEnv: Record<string, string | undefined> = {};
beforeAll(() => {
// Pin the resolver inputs so the defaults below are the true defaults
// (no CLINE_DIR/CLINE_DATA_DIR overrides, known home directory).
for (const key of ["CLINE_DIR", "CLINE_DATA_DIR"]) {
savedEnv[key] = process.env[key];
delete process.env[key];
}
setHomeDir(FAKE_HOME);
});
afterAll(() => {
for (const [key, value] of Object.entries(savedEnv)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});
it("reports the actual resolver defaults for --config and --data-dir", () => {
// A wide help width keeps each option description on one line so the
// full default text can be matched.
const help = createProgram()
.configureHelp({ helpWidth: 500 })
.helpInformation();
const configDefault = tildePath(resolveClineDir(), FAKE_HOME);
const dataDirDefault = tildePath(resolveClineDataDir(), FAKE_HOME);
// Sanity-check the resolvers themselves so the assertions below can't
// silently drift along with a resolver regression.
expect(configDefault).toBe("~/.cline");
expect(dataDirDefault).toBe("~/.cline/data");
expect(help).toContain(
`Configuration directory (default: ${configDefault})`,
);
expect(help).toContain(
`Use isolated local state at this directory path (default: ${dataDirDefault})`,
);
});
});
+4 -6
View File
@@ -33,7 +33,7 @@ export function addRootOptions(cmd: Command): Command {
.option("-c, --cwd <path>", "Working directory")
.option(
"--thinking <level>",
"Set reasoning effort level between none|low|medium|high|xhigh (default: medium)",
"Set reasoning effort: none|low|medium|high|xhigh. Bare --thinking uses medium; omitted leaves provider default.",
)
.option("--compaction <mode>", CLI_COMPACTION_MODE_OPTION_DESCRIPTION)
.option(
@@ -64,13 +64,10 @@ export function addRootOptions(cmd: Command): Command {
"--acp",
"Run in Agent Client Protocol (ACP) mode for editor integration",
)
.option(
"--config <path>",
"Configuration directory (default: ~/.cline/data/settings)",
)
.option("--config <path>", "Configuration directory (default: ~/.cline)")
.option(
"--data-dir <path>",
"Use isolated local state at this directory path (default: ~/.cline)",
"Use isolated local state at this directory path (default: ~/.cline/data)",
)
.option(
"--hooks-dir <path>",
@@ -136,6 +133,7 @@ export function commanderToParsedArgs(program: Command): ParsedArgs {
interactive: !!opts.tui,
outputMode: opts.json ? "json" : "text",
mode: opts.plan ? "plan" : opts.yolo ? "yolo" : opts.zen ? "zen" : "act",
modeExplicitlySet: !!(opts.plan || opts.act || opts.yolo || opts.zen),
sandbox: !!opts.dataDir,
acpMode: !!opts.acp,
thinking: false,
+177 -4
View File
@@ -6,11 +6,29 @@ import { createScheduleCommand } from "./schedule";
const mockSendHubCommand = vi.hoisted(() => vi.fn());
const mockEnsureCliHubServer = vi.hoisted(() => vi.fn());
vi.mock("@cline/core", () => ({
sendHubCommand: mockSendHubCommand,
const mockProviderSettings = vi.hoisted(() => ({
lastUsed: undefined as { provider?: string; model?: string } | undefined,
providers: {} as Record<string, { provider?: string; model?: string }>,
}));
vi.mock("@cline/core", async () => {
const actual =
await vi.importActual<typeof import("@cline/core")>("@cline/core");
return {
...actual,
sendHubCommand: mockSendHubCommand,
ProviderSettingsManager: class {
getLastUsedProviderSettings() {
return mockProviderSettings.lastUsed;
}
getProviderSettings(providerId: string) {
return mockProviderSettings.providers[providerId];
}
},
};
});
vi.mock("../utils/hub-runtime", () => ({
ensureCliHubServer: mockEnsureCliHubServer,
parseHubEndpointOverride: (rawAddress: string | undefined) => {
@@ -47,6 +65,8 @@ async function runScheduleCommand(
describe("runScheduleCommand list output", () => {
afterEach(() => {
vi.clearAllMocks();
mockProviderSettings.lastUsed = undefined;
mockProviderSettings.providers = {};
});
it('prints "No schedules found." for empty non-json list output', async () => {
@@ -121,9 +141,158 @@ describe("runScheduleCommand list output", () => {
});
});
describe("runScheduleCommand create delivery metadata", () => {
describe("runScheduleCommand create", () => {
afterEach(() => {
vi.clearAllMocks();
mockProviderSettings.lastUsed = undefined;
mockProviderSettings.providers = {};
});
it("uses the last used provider and model when both flags are omitted", async () => {
mockProviderSettings.lastUsed = {
provider: "anthropic",
model: "claude-sonnet-4-6",
};
mockEnsureCliHubServer.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
authToken: "test-token",
});
mockSendHubCommand.mockResolvedValue({
ok: true,
payload: { schedule: { scheduleId: "sched_123" } },
});
const output: string[] = [];
const errors: string[] = [];
const code = await runScheduleCommand(
[
"create",
"Health check",
"--cron",
"0 */6 * * *",
"--prompt",
"Run tests",
"--workspace",
"/tmp/workspace",
"--address",
"127.0.0.1:25463",
],
{
writeln: (text?: string) => {
output.push(text ?? "");
},
writeErr: (text: string) => {
errors.push(text);
},
},
);
expect(code).toBe(0);
expect(errors).toEqual([]);
expect(mockSendHubCommand).toHaveBeenCalledWith(
{ host: "127.0.0.1", port: 25463, pathname: "/hub" },
expect.objectContaining({
clientId: "cline-schedule",
command: "schedule.create",
payload: expect.objectContaining({
provider: "anthropic",
model: "claude-sonnet-4-6",
}),
}),
);
});
it("uses an explicit provider with that provider's configured model", async () => {
mockProviderSettings.lastUsed = {
provider: "cline",
model: "openai/gpt-5.3-codex",
};
mockProviderSettings.providers.anthropic = {
provider: "anthropic",
model: "claude-sonnet-4-6",
};
mockEnsureCliHubServer.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
authToken: "test-token",
});
mockSendHubCommand.mockResolvedValue({
ok: true,
payload: { schedule: { scheduleId: "sched_123" } },
});
const errors: string[] = [];
const code = await runScheduleCommand(
[
"create",
"Health check",
"--cron",
"0 */6 * * *",
"--prompt",
"Run tests",
"--workspace",
"/tmp/workspace",
"--provider",
"anthropic",
"--address",
"127.0.0.1:25463",
],
{
writeln: () => {},
writeErr: (text: string) => {
errors.push(text);
},
},
);
expect(code).toBe(0);
expect(errors).toEqual([]);
expect(mockSendHubCommand).toHaveBeenCalledWith(
{ host: "127.0.0.1", port: 25463, pathname: "/hub" },
expect.objectContaining({
command: "schedule.create",
payload: expect.objectContaining({
provider: "anthropic",
model: "claude-sonnet-4-6",
}),
}),
);
});
it("fails when an explicit provider has no configured model and no model flag", async () => {
mockEnsureCliHubServer.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
authToken: "test-token",
});
const errors: string[] = [];
const code = await runScheduleCommand(
[
"create",
"Health check",
"--cron",
"0 */6 * * *",
"--prompt",
"Run tests",
"--workspace",
"/tmp/workspace",
"--provider",
"anthropic",
"--address",
"127.0.0.1:25463",
],
{
writeln: () => {},
writeErr: (text: string) => {
errors.push(text);
},
},
);
expect(code).toBe(1);
expect(errors).toEqual([
'No model is configured for provider "anthropic". Pass --model or save a model for that provider before creating the schedule.',
]);
expect(mockSendHubCommand).not.toHaveBeenCalled();
});
it("maps --delivery-bot to delivery.userName", async () => {
@@ -192,6 +361,8 @@ describe("runScheduleCommand create delivery metadata", () => {
describe("runScheduleCommand import", () => {
afterEach(() => {
vi.clearAllMocks();
mockProviderSettings.lastUsed = undefined;
mockProviderSettings.providers = {};
});
it("preserves exported modelSelection providerId/modelId values", async () => {
@@ -257,6 +428,8 @@ describe("runScheduleCommand import", () => {
describe("runScheduleCommand export", () => {
afterEach(() => {
vi.clearAllMocks();
mockProviderSettings.lastUsed = undefined;
mockProviderSettings.providers = {};
});
it("writes JSON content to the --to file path", async () => {
+4 -2
View File
@@ -148,8 +148,10 @@ export function isJsonPath(path: string): boolean {
return path.toLowerCase().endsWith(".json");
}
export function parseMode(raw: string | undefined): "act" | "plan" | undefined {
if (raw === "act" || raw === "plan") {
export function parseMode(
raw: string | undefined,
): "act" | "plan" | "yolo" | undefined {
if (raw === "act" || raw === "plan" || raw === "yolo") {
return raw;
}
return undefined;

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