Compare commits

..
Author SHA1 Message Date
+15 84c59f93a2 chore(desktop): bump beta to 0.0.23-beta.1 (#13782)
* fix(vscode): continue the surviving session on resume instead of rebuilding with the original task text (#13175)

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilt the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

The preserved conversation history is the source of truth on resume, so
the fallback prompt now just asks the model to reassess the history and
continue, matching the legacy resume prompt which also never resent the
original task. User-typed text still takes precedence when provided.

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

* fix(vscode): continue the surviving idle session on follow-ups instead of rebuilding

Stopping a turn keeps the session alive, but every idle follow-up (bare
Resume after Stop, and typed follow-ups after a completed turn) tore
that session down and rebuilt it from persisted task history before
sending. Continue the matching idle session in place instead, the same
way the CLI reuses the live session after an abort. Rebuilding from
history now only happens when no live session matches the displayed
task (task opened from history, extension host reload).

A bare resume still needs a prompt to start a turn, so it sends the
neutral [TASK RESUMPTION] prompt (shared with the rebuild fallback and
hidden from the transcript); user-typed content is echoed and sent
as-is. If the send lands while the abort is still settling, the runtime
auto-queues it and drains once the abort completes.

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

* refactor(vscode): consolidate follow-up send paths in SdkFollowupCoordinator

Now that idle follow-ups continue the live session in place, the
two-mode sendToActiveSession helper was redundant: its non-queued branch
duplicated continueIdleSession minus the bare-resume prompt. Split it
into a single-purpose queueToActiveSession and fold the idle no-task
send into continueIdleSession, flattening askResponse's decision tree
to: queue onto a running turn, continue a matching live idle session,
rebuild from history, or abandon.

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

* refactor(vscode): reuse the existing neutral resumption prompt for bare resumes

Drop the newly invented long resumption wording in favor of the phrase
that already existed as the no-history fallback and that the transcript
hiding logic and test fixtures recognize: '[TASK RESUMPTION] Please
continue where you left off.' The net change to resumeSessionFromTask
against main is now just deleting the branch that resubmitted
historyItem.task as new instructions.

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

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilds the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

Bare resumes now always use the neutral prompt that already existed as
the no-history fallback; user-typed text still takes precedence. This
matches the legacy resume prompt (responses.taskResumption), which only
ever included user-supplied text as new instructions.

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

* fix(vscode): hide synthetic prompts from the queued-prompt echo

A send that races a settling abort is auto-queued by the runtime, so a
bare Resume can reach the pending_prompt_submitted echo carrying the
synthetic [TASK RESUMPTION] prompt. Echoing it leaked model-facing text
as a visible user bubble and shifted the visible-user-message ordinals
that edit/regenerate mapping relies on. Filter synthetic prompts with
isSyntheticUserPrompt, keeping user attachments visible (matching
isSyntheticSdkUserMessage semantics).

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

---------

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

* fix(vscode): preserve LiteLLM input token limits (#13293)

* fix(vscode): preserve LiteLLM input token limits

* fix(vscode): prefer live LiteLLM model metadata

* fix(vscode): generalize private catalog metadata

* test(vscode): preserve llms exports in vscode lm mock

* fix(vscode): point provider signup URLs at their API key pages (#13337)

* fix(vscode): point Mistral signup URL at the general API keys console

The Mistral provider's signup link led to the Codestral console, which
issues Codestral-scoped keys that fail with 401 on api.mistral.ai — the
endpoint the provider actually calls. Point it at the general API keys
page instead.

Fixes #13288

* fix(vscode): deep-link DeepSeek and Fireworks signup URLs to their API key pages

Both pointed at marketing homepages; link straight to the key-creation
pages instead, matching the rest of the registry and the desktop app's
provider-key-urls map.

* fix(ci): always build the legacy bundle from the legacy-extension branch (#13349)

The combined-VSIX workflow took legacy-ref as a free-form dispatch input
with no publish-time validation (next-ref has one: publish requires main).
Any typed ref — a PR merge ref, an unprotected branch — would be built
into the published VSIX by the environment-less build job, and the publish
environment approver only ever sees an opaque prebuilt artifact, so the
approval protected the marketplace PAT but not the shipped bytes.

Remove the input entirely and hardcode the protected legacy-extension
branch, which makes that branch's protection rules load-bearing for
releases. The tested-sha pinning between test-legacy and build is
unchanged. publish-extension skill dispatch command updated to match.

* fix(ci): lock the legacy publish workflow to the legacy-extension branch (#13350)

The branch dispatch input was a free-form string with no validation. Both
jobs checked it out and ran full npm lifecycle scripts from it: the publish
job next to VSCE_PAT/OVSX_PAT (and npm run publish:marketplace executes a
script from that same ref with the PATs in env), and the test job with NO
environment approval at all while inheriting the workflow-level
contents/packages/checks/pull-requests write grants. A dispatch pointing at
e.g. refs/pull/N/head would run outside-contributor code with the
marketplace keys behind one approval, or with a repo-write token behind
none.

Remove the input and hardcode the protected legacy-extension branch, drop
the workflow-level permissions to contents: read, and elevate only the
publish job to contents: write (tag push + GitHub release). The branch
input's default was legacy-extension, so normal publishes are unchanged.
publish-extension skill dispatch command updated to match.

* fix(vscode): SDK remote-config parity — refresh coordination, session gating, and fail-closed opt-out (#13226)

* feat(desktop): native notifications (#13166)

* feat(desktop): native notifications

* macos target

* fix(desktop): isolate macOS dev app identity

* fix(desktop): address notification review feedback

---------

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

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched (#13310)

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched

Toggling an auto-approve setting while a task is open writes
autoApprovalSettings into the StateManager's task-settings overlay
(updateAutoApprovalSettings -> setTaskSettings). The SDK controller never
cleared that overlay on clearTask/showTaskWithId (the legacy controller
did), so after New Task the stale overlay kept shadowing global settings
in getGlobalSettingsKey(): toggle RPCs were accepted into global state,
but every posted state still carried the overlay's old version, which the
webview rejects as not newer - the auto-approve checkboxes froze forever.

Restore legacy parity in SdkTaskControlCoordinator: drop the overlay
(persisting pending writes first) in clearTask() and before installing a
different task's proxy in showTaskWithId().

Fixes #13260

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

* changeset

* test(vscode): add end-to-end regression test for auto-approve freeze after New Task

Wires the real StateManager, the real updateAutoApprovalSettings handler,
and the real SdkTaskControlCoordinator.clearTask() together with the
webview's version gate modeled on ExtensionStateContext, pinning the
end-to-end invariant behind #13260: checkbox toggles must keep reaching
the webview after a mid-task toggle followed by New Task. Verified the
test fails when the clearTaskSettings() call is removed from clearTask().

* fix implicit any in regression test

---------

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

* feat(desktop): show provider web-search support under the settings toggle (#13328)

* feat(desktop): show provider web-search support under the settings toggle

The global Web search toggle silently does nothing unless the session's
provider offers native web search, which made the setting read as if it
worked with any provider. The desktop General settings row now explains
that only providers with built-in web search honor it, and shows a live
status line: which connected providers are ready to use it (no extra
setup needed), or an amber warning with a link to the Models section
when none of them support it.

Support is resolved in the webview via a new providerOffersModelTool
helper in @cline/llms (browser export), sharing the same builtin-manifest
source of truth as the runtime's supportsModelTool attachment check.

* fix(desktop): address review — refetch web-search status on catalog invalidation, clarify per-model support

Greptile P2: the one-time catalog fetch could race an in-flight provider
save and show stale status; the row now refetches when the provider
catalog cache is invalidated (fired after saves complete).

Greptile P1: the ready line implied every model on the provider works;
Vertex excludes Claude routes, so the copy now scopes the promise to
models that support it.

---------

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

* feat(ui/desktop): collapse finished runs into a work summary and remove hover-state dead space (#13315)

* feat(ui): add WorkActivity collapsed-run summary and float message actions as a pill

WorkActivity/WorkActivityTrigger/WorkActivityContent fold a finished agent
run's working rows (tool calls, thinking traces, narration) behind a single
"Worked for 4m 12s · 14 tool calls" disclosure built on the shared animated
disclosure primitives, with formatWorkActivityLabel/formatWorkDuration
exported for consumers.

Message hover actions no longer rely on the transcript reserving blank space
below each message: the action row is now a self-backed pill (border,
blurred background, shadow) that floats over whatever follows, so
conversations can pack rows tightly without hover chrome colliding with the
next message.

* feat(desktop): collapse finished runs into a work summary and tighten chat spacing

collapseCompletedWork post-processes the grouped transcript: once a run ends
on assistant text with no further tool calls, its working rows fold into one
expandable WorkActivity row while the final answer stays visible. Runs are
delimited by user messages; the trailing run only collapses when the session
has stopped running and actually produced an answer, so live streams and
cancelled/failed tails keep their rows. Assistant messages carrying images
or media are treated as deliverables and never collapse.

The conversation list gap drops from gap-8 to gap-4 now that hover actions
are self-backed pills that need no reserved space, and user messages add
their own top margin so turn boundaries stay visually distinct.

* refactor(ui/desktop): work summary label wording, flat expansion, stable in-run rhythm

Feedback round on #13315:

- Label reads "Worked for 4m 12s and made 14 tool calls" instead of joining
  with a dot; without a duration it falls back to "Made N tool calls".
- Expanded work rows render at transcript level — no rail or extra indent —
  since tool rows and thinking traces already carry their own nesting when
  expanded. The work content keeps the tight working-row rhythm.
- Live working rows (thinking traces + tool calls) now group into a 'run'
  render item with the same tight 0.25rem rhythm, so there is no oversized
  gap under a "Thought for Ns" row and every row keeps its exact position
  when the finished run folds into the work summary. A trailing
  answer-in-progress stays outside the group at transcript level, and pure
  prose spans keep normal spacing.
- The transient "Thinking..." indicator moves inside the transcript column
  and mirrors a trigger row's geometry, so the first real row replaces it in
  place with no jump.

* style(ui/desktop): hover-pill metrics, right-pointing work chevron, scroll and spacing fixes

Another feedback round on #13315:

- Hover action pill: +2px internal padding, a trailing inset after the
  timestamp (it sat flush against the pill border), and more clearance
  between the message content and the pill (2px -> 6px; the hover bridge
  grows to match).
- The work summary chevron points right while collapsed and continues
  counterclockwise to point up when expanded.
- Conversation bottom padding drops pb-20 -> pb-8: the composer sits below
  the scroller, so the padding only needs to clear a pinned action pill.
- Sending a message scrolls back to the bottom even if the reader had
  scrolled up (new AutoScrollOnSend on the user-message count, which ignores
  optimistic-bubble re-keying; @cline/ui now exports useConversation for
  this).
- An assistant answer directly under its run's working rows pulls itself
  0.5rem closer than the full transcript gap.

* style(desktop): leave a visible gap between a pinned action pill and the composer

pb-8 exactly matched the pill's ~40px footprint, so the last row's hover
actions sat flush against the composer top; pb-12 restores ~8px of daylight.

* style(desktop): widen the gap between the pinned action pill and the composer to ~24px

pb-12 left only ~8px of daylight under the pill; pb-16 reads comfortable
without reverting to pb-20's dead space.

* fix(desktop): keep the thinking indicator at the working-row offset mid-run

The indicator matched a trigger row's geometry but sat a full transcript gap
(1rem) below the last working row, while the tool/thinking row replacing it
joins the tight run group at 0.25rem — a visible upward jump. When the last
transcript item is working rows (or streamed assistant output), the
indicator now pulls up to the same tight offset; only at the start of a run,
under the user message, does it keep the normal gap.

* style(ui): calm the hover actions surface per team feedback

Borderless rectangle instead of the bordered pill: radius drops to
var(--radius), the side padding goes entirely (the icon buttons carry their
own hit areas), and the vertical padding halves. Blurred background and
shadow stay so it remains legible over following content.

* feat(ui/desktop): full-band hover reveal and iOS-style disclosure easing

The hover actions only appeared while the pointer was inside the message
box itself. The invisible bridge under each message now spans the full
height of the band the floating actions occupy (full row width), so
hovering anywhere in that strip reveals them. Sibling row types
(.cline-chat-tool, .cline-chat-work, and the desktop's run/tool groups)
become position: relative so they paint above the bridge — their own
content keeps its hover and clicks, and the bridge only wins in the band's
genuinely empty space.

All expandable rows (work summary, tool panels, thinking) open and close on
a 240ms symmetric ease-in-out cubic-bezier instead of the 60ms snap, with
chevron rotation on the same curve. Reduced-motion still disables both.

* revert(ui/desktop): drop the full-band hover reveal; quicken disclosure easing to 180ms

The full-band hover bridge (and the position: relative changes that made it
safe) is reverted per feedback — back to the narrow bridge that only spans
the gap under the message. The iOS-style ease-in-out on disclosures stays
but speeds up from 240ms to 180ms.

* fix(ui): recover live tool diffs that mount as a blank pierre skeleton

Live-streamed edit rows could show an empty diff for the whole run, with the
diff only appearing after the collapsed work row was expanded (fresh mount).
Root cause, confirmed by driving a live session and inspecting the element:
React StrictMode double-invokes @pierre/diffs' ref callback; the first
instance's async highlight work aborts on its immediate cleanup, and the
second instance adopts the abandoned half-rendered shadow tree as if it were
complete prerendered output — zero height, no code, no theme stylesheet,
permanently. A rendered diff always carries style[data-theme-css] in its
shadow root, so ToolFileDiff now checks for it shortly after mount and
remounts FileDiff (bounded attempts) when missing; the fresh host element
takes the normal render path and recovers within ~400ms. Verified live: the
diff now renders during the run.

* fix(desktop): keep interrupted runs expanded even with partial trailing text

The trailing-run collapse gated on 'ended with assistant text', which
misread a Stop that landed mid-answer as a finished run and folded the tool
calls the user wants to inspect. The gate is now the terminal status itself:
only completed (or restored-idle) sessions collapse the trailing run;
cancelled/failed/error tails keep their rows regardless of partial text.
(Greptile P1 on #13315 — matches the PR's stated rule.)

* feat(ui): share the markdown pipeline, chat polish, and ThinkingBlock across products (#13323)

* feat(ui): share the markdown pipeline, chat polish CSS, and ThinkingBlock

The desktop app and the cloud dashboard both consume @cline/ui yet rendered
assistant output differently, because Markdown policy and the thinking-trace
row lived app-side. This moves the shareable parts into the package:

- components/markdown (new export): the lazy Shiki code highlighter (GitHub
  light/dark, pinned language set) and agentMarkdownControls — the standard
  Streamdown configuration. streamdown/shiki/@shikijs/* become optional peer
  dependencies, mirroring @pierre/diffs.
- components/markdown.css: the desktop's chat polish moves in — chat-scale
  headings, outside list markers, single quiet code blocks with a
  hover-revealed copy control, table cards. Kept unlayered so it beats
  Streamdown's layered Tailwind utilities without !important.
- ThinkingBlock + formatThoughtLabel in agent-chat: the standard thinking
  row (brain icon, Thinking/Thought-for-Ns label, streaming shimmer, rail
  presentation, capped scrollable body). The shimmer and the
  reasoning-hover-suppression rule move into agent-chat.css; triggers gain
  the color transition the desktop applied locally.

Version bumps to 0.2.0-next.5 for the dashboard to pick up.

* refactor(desktop): consume shared markdown and thinking primitives from @cline/ui

The local Shiki highlighter, Streamdown controls, chat markdown polish CSS,
streaming-title shimmer, and reasoning hover-suppression rule are deleted in
favor of the @cline/ui versions (the highlighter test moves to the package's
suite). ReasoningBlock becomes a thin wrapper that hands MemoizedMarkdown to
the shared ThinkingBlock, and formatThoughtLabel re-exports from the package
so grouping code and tests keep their import path.

globals.css now imports @cline/ui/components/markdown.css (unlayered, so the
polish keeps beating Streamdown's layered utilities); the app keeps only what
is genuinely app-specific: link/image policy in markdown.tsx, selectability
rules, accent palettes, and the view-enter transition.

* style(ui/desktop): make thinking-trace prose legible

Thinking body text rendered too faint: plain muted-foreground plus the
desktop's font-thin weight. The shared thinking content now leans 75% of the
way back toward the body text color (still slightly de-emphasized), and the
desktop drops the thin font weight.

* ci(ui-publish): build @cline/shared before ui typecheck (#13354)

@cline/ui's generated-media imports @cline/shared/browser, which resolves to
shared's dist output. The build-shared step sat after typecheck/test/build,
so the first ui-publish dispatch since #13025 failed at Typecheck UI with
TS2307. Move the step to right after install.

* fix: run_commands object form without args routes through the shell instead of failing with ENOENT (#13336)

* fix: run_commands object form without args routes through the shell

The structured { command, args? } form of run_commands was always spawned
directly with shell: false. When a model emitted a full command line in
command with no args (e.g. { command: "echo hello" }), spawn failed with
ENOENT for any command containing a space, breaking command execution for
the whole session.

Direct exec now only applies when a non-empty args list is provided; the
object form without args is routed through getShellInvocation like the
string form. Schema descriptions are tightened so models put arguments in
args instead of embedding them in command.

Fixes #13279

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

* chore: trim structured-command schema descriptions

The union schema is only used for lenient validation of input the model
already sent; its descriptions never reach a model prompt. Keep them
short instead of restating executor behavior.

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

* chore: simplify direct-exec comment in shell executor

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

* revert: keep original structured-command schema description

The description never reaches a model prompt and the executor now handles
both shapes, so the wording change was cosmetic noise.

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

* fix: gate direct exec on args key presence, not array length

Review feedback: an explicit empty args array is intentionally structured
input and stays direct exec; only an object with no args key is treated
as a full shell command line. Matches the key-presence rule already used
by the VS Code host's formatCommandForTerminal. Also replaces the
empty-args shell test (which was PowerShell-incompatible) with a test
pinning the direct-exec contract.

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

---------

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

* fix: normalize Gemini custom base URLs for legacy host-root values (#13329)

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

* docs: add GLM-5.3 to ClinePass models and reference pricing (#13357)

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

* feat(desktop): stream run command output (#13179)

* feat(desktop): stream run command output

* fix(sdk): clean up detached command logs

* fix(sdk): reap detached logs after hub restarts

* fix(sdk): preserve live detached command logs

* fix(desktop): harden live command progress

* fix(sdk): recover detached logs for local hosts

* fix(desktop): reconcile command output tool rows

* fix(sdk): retain logs for surviving commands

* fix(core): prevent PID reuse from retaining detached logs

* fix(core): preserve detached logs on probe failures

* fix(core): retain detached logs during probe outages

* fix(desktop): resolve leftover merge conflict in messages projection test

Combine both sides of the assertion: main's incremented per-block
createdAt projection and this branch's toolCallId/hookEventName meta.

---------

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

* fix(cli): make TUI dialog colors follow theme changes live (#13355)

* fix(cli): make TUI dialog colors follow theme changes live

Dialog content previously read the static palette constant, so open
dialogs (including the theme picker itself) kept the default dark-blue
accents while scrolling through theme previews. Add getDialogPalette /
useDialogPalette, which resolve dialog colors from the active theme's
dialog accents and re-render on every theme change, and migrate all
dialog-rendered components to it.

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

* feat(cli): derive dialog panel background from the active theme

Dark themes now lift their own background one OKLAB step for the dialog
surface, so panels keep the theme's hue instead of the library's fixed
#262626. DialogThemeSync pushes the surface into the dialog container
for new dialogs and repaints open panels, so the surface also follows
live theme previews. Light themes keep the neutral dark panel to match
the dark accent fallback and the light-on-dark dialog text.

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

---------

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

* fix: skill slash commands load via the skills tool instead of expanding into the user message (#13327)

* fix(desktop): show typed slash command instead of expanded skill markdown

The sidecar expands /skill and /workflow tokens into their instructions
before dispatching, so the runtime's persisted transcript only contains the
expanded text. After a turn (and when reopening a session) the webview
re-hydrates from that history and rendered the whole SKILL.md body as the
user's message; queue events echoing the expanded prompt could also add a
second user bubble, and fresh sessions were titled with the markdown's first
line. The CLI never shows this because its TUI keeps the typed text in its
own transcript and only sends the expanded prompt to the model.

Mirror that separation inside the desktop sidecar's display boundaries:

- history projection (readSessionMessages) inverts user text that starts
  with a configured command's instructions back to '/name remainder',
  which also repairs sessions recorded before this fix
- queue snapshots and chat_queued_prompt_start events echo the typed
  prompt recorded at expansion time, so the webview's optimistic-bubble
  re-key matches again
- an untitled session sent an expanded prompt gets titled from the typed
  command instead of the instructions' first line

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

* fix(desktop): don't overwrite a mid-turn rename with the typed-command title

The untitled check ran before dispatch, so renaming a fresh slash-command
session while its first turn was running got clobbered by the post-turn
typed-command title. Re-check at write time and only replace a missing title
or the one the runtime auto-derived from the expanded prompt.

Also documents the inherent prefix-inversion ambiguity flagged in review:
text hand-typed with a command's exact instructions persists byte-identically
to that command's expansion, so stored history alone cannot distinguish them.

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

* fix(desktop): stop expanding skill commands; let the skills tool load them

Pasting the skill body into the prompt is why the transcript could ever show
it: the desktop webview re-hydrates from the runtime's persisted history, so
whatever the sidecar splices into the user message renders as if the user
typed it. The runtime already registers the skills tool, whose description
requires the model to invoke it whenever the user references a slash command
— so send the typed /skill text through and let the tool deliver the
instructions as a tool result (previously they arrived twice: pasted and via
the tool). The persisted user message, session title, and queue entries are
then simply the typed command, which deletes the typed-prompt registry, the
queue event/snapshot rewriting, and the title machinery from the previous
approach.

Workflows are not served by the skills tool and keep textual expansion, so
the read-time display inverter stays: it collapses expanded workflow prompts
— and skill prompts persisted before this change — back to the typed
/command in the history projection.

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

* feat(core): option to keep skill slash commands typed for the skills tool

resolveRuntimeSlashCommandFromWatcher (and the hub snapshot proxy) accept
expandSkillCommands: hosts whose sessions register the skills tool pass
false so the typed /skill goes through and the model loads the instructions
as a tool result, keeping the persisted transcript as what the user typed.
Workflows always expand — the tool does not serve them. isSkillsToolAvailable
exposes the catalog check hosts use to decide (yolo preset and the skills
tool toggle leave textual expansion as the only delivery path).

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

* fix(cli): skill slash commands load via the skills tool instead of expanding

The TUI user-command wrap and buildUserInputMessage now keep a typed /skill
as-is when the session's mode/toggles register the skills tool, matching the
desktop app; workflows keep expanding, and yolo (zen) keeps expanding skills
because its preset has no skills tool. This also fixes CLI resume/history
surfaces showing the skill body: the persisted user message is now the typed
command.

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

* fix(vscode): keep configured skill slash commands typed for the skills tool

expandSlashCommands no longer splices a configured skill's instructions into
the model text; the SDK session's skills tool delivers them as a tool result
(previously they arrived twice). Builtin pseudo-skills like /deep-planning
are not served by that tool and keep expanding, as do workflows.

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

* refactor(desktop): use the shared skill-expansion option in the sidecar

Replaces the sidecar's workflow-detection dance with core's
expandSkillCommands option and gates on isSkillsToolAvailable, restoring
textual expansion where the tool is missing (yolo mode or the skills tool
toggle) — a gap in the previous desktop-only change.

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

* refactor(desktop): drop the display inverter for expanded transcripts

Accepted trade-off to keep the change minimal: sessions recorded before
skills switched to the skills tool, workflow sends (deprecated), and
yolo-mode skill sends persist expanded instructions and now render that text
as-is instead of being collapsed back to the typed /command at projection
time.

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>

* Use fixed selection chevron in account dialog to match other dialogs (#13364)

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

* fix(desktop): align system prompt with session mode (#13361)

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

* fix(desktop): finalize queued turns on chat_done with canonical history reconcile (#13330)

Turns that settle through the event stream (queued prompts, including the
first prompt of a fresh session) resolve their send() RPC early, so nothing
cleared the streaming shimmer or reconciled live-streamed content against
the persisted transcript at turn end. A turn whose deltas were incomplete
stayed visually streaming forever and only healed when a later non-queued
send rehydrated history.

chat_done (and chat_session_ended / the queue-drain double check) now clears
the active assistant streaming id and schedules a short-delayed
read_session_messages + applyCanonicalHistory, guarded by turn epoch,
session id, and in-flight send submissions so it never clobbers a newer
turn or duplicates the blocking send path's own finalization.

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

* chore(desktop): release v0.0.14

* fix(clients): filter non-chat models from chat pickers (#13317)

* fix(clients): filter non-chat models from chat pickers

* fix(clients): align chat model eligibility

* fix(desktop): strip user_input envelope when copying a user message (#13369)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>

* docs: show DeepSeek V4 peak and off-peak pricing (#13312)

* docs: update DeepSeek V4 average pricing

* docs: show DeepSeek peak and off-peak pricing

* docs: add GLM-5.3 reference pricing (same as GLM-5.2)

* docs: add GLM-5.3 to ClinePass models table

* fix(llms): display billed gateway cost (#13385)

* fix(shared): run PowerShell commands with fail-fast error semantics (#13358)

* fix(shared): run PowerShell commands with fail-fast error semantics

The run_commands PowerShell wrapper never set $ErrorActionPreference, so
the default 'Continue' applied: a pipeline erroring per item (e.g. a
malformed Where-Object over Get-ChildItem -Recurse) emitted one error
record per enumerated file - tens of thousands of stderr records on
large trees, looking like a hang - and could still resolve as SUCCESS
with exit 0.

Prepend $ErrorActionPreference='Stop'; to the script content executed
by the ScriptBlock so the first error terminates the command with a
non-zero exit and a single error message. Concatenated on the same line
as the user command so error line numbers stay unshifted.

Fixes #13285

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

* fix(shared): set the fail-fast preference in the bootstrap scope

Setting $ErrorActionPreference='Stop' by string-prepending it into the
scriptblock source displaced a leading param(...) from its mandatory
first-statement position, so scripts beginning with a param block failed
with CommandNotFoundException. Preference variables are dynamically
scoped, so setting Stop in the -Command bootstrap gives the invoked
scriptblock identical fail-fast semantics while keeping the user script
byte-identical (param works, error positions unshifted) and drops the
doubled-quote escaping.

* docs(shared): document the fail-fast tradeoffs in the PowerShell wrapper

Stop promotes every non-terminating error, not only per-item pipeline
floods: partial-result commands (recursive listings over access-denied
junctions) now stop at their first error, and Windows PowerShell 5.1
turns in-script stderr redirection of succeeding native commands fatal.
State this in the wrapper comment as a deliberate tradeoff, with the
GitHub Actions precedent and the per-command opt-outs.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* ci: stop over-long changelogs from silently dropping release Slack posts (#12955)

Slack section blocks reject text longer than 3000 characters. The Slack
action logs that rejection as ##[error] but does not fail the step, so an
over-long changelog drops the release announcement while the run stays
green — cline@3.0.50 (3272 chars) published to npm, tagged, and cut a
GitHub release with no Slack post and nothing red to notice.

Every publish workflow pasted the changelog section verbatim into one
section block, so all six were exposed; the SDK, desktop, and extension
sections were only 150-350 chars under the ceiling.

Add a slack_content output alongside content: unchanged when the section
fits, otherwise trimmed on a line boundary with a link to the full
release notes. Only the Slack payload uses it — GitHub release bodies and
the desktop updater manifest still get the whole section.

* ci: tidy workflow cache config and job permissions (#13403)

Publish workflows now always do clean npm installs (no dependency
cache in their test gates), the e2e workflow's cache keys are
exact-match only, and the e2e job drops an id-token permission it
never used.

* Rename desktop app from "Cline Code" to "Cline" (#13401)

* Rename desktop app from Cline Code to Cline

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

* Format touched Rust test assertions

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

---------

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

* fix(llms): surface provider-executed tool activity as observational events (#13300)

* fix(llms): surface provider-executed tool activity as observational events

Provider-executed tool parts (e.g. every tool the Claude Code CLI runs
inside its own session) were dropped by the model-tool guard added for
web search: only declared model tools were re-emitted, everything else
hit continue with nothing yielded. Those sessions modified the workspace
with no tool activity in runtime events, transcripts, or the UI.

Route all providerExecuted parts onto the observational path instead:
emit execution-tagged tool-call-delta and tool-result events, matched by
tool-call ID for providers that omit the flag on the result half. They
stay out of AgentRuntime's execution/approval loop, and the runtime
already persists them as modelToolActivities and projects them for
display.

The AgentModelEvent tool-result variant widens toolName from
ModelToolName to string to carry the provider's own tool names.

* fix(agents): keep turns that are only provider-executed tool activity

A turn consisting solely of observational tool activity has an empty
assistant content array - the activity lives in message metadata, since
projecting it into content would replay tool_use blocks the model never
gets results for. The empty-content guard threw on such turns, erroring
the run and losing the activity from the transcript. Count model-tool
activity as content for the emptiness check (error finishes still
throw); replay stays safe through the codec's empty-content placeholder.
Also drop the trailing text delta from one gateway test so the tool-only
stream shape stays covered end to end.

* feat: allow agents to create scheduled tasks (#13331)

* feat(core, desktop): add durable todo agenda

* fix(desktop): secure todo approvals and track tool usage

* fix(desktop): clean up failed approval delivery

* fix(desktop): authenticate approval connections

* fix(desktop): cancel approvals on broadcast failure

* fix(desktop): authenticate development approvals

* fix(desktop): harden development approvals

* test(core): make task paths cross-platform

* fix(desktop): serialize approval readiness

* refactor(core): unify todo and schedule tools

* feat(core): distinguish user todos from agent suggestions

* fix(core): hide tasks tool in yolo mode

* fix(core): enforce schedule workspace scope

* fix(core): bind schedule scope to hub connection

* fix(core): establish task scope at hub startup

* fix(core): scope task automation by workspace

* test(core): normalize workspace path expectations

* test(core): serialize Windows CI workers

* fix(core): reject unregistered schedule authority

* fix(desktop): guard task execution commands

* fix(core): avoid polynomial regex in mention parsing

* fix(core): address schedule tool review feedback

* fix(core): bind websocket clients to hub workspace

* fix(core): flatten tasks tool input schema

* fix(core): authorize multi-workspace hub clients

* test(core): type hub transport authority mock

* fix(cli): register a workspace client for remote schedule commands (#13398)

---------

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

* fix(desktop): treat ClinePass as OAuth-managed in the chat credential gate (#13404)

* fix(desktop): treat ClinePass as OAuth-managed in chat credential gate

ClinePass shares the Cline account OAuth credentials (its auth handler
stores under the "cline" provider), so the webview never sees a plain
API key for it. The chat pre-flight check only exempted cline/oca/
openai-codex, so switching to ClinePass while signed in via OAuth
blocked with "Missing API key" even though the sidecar resolves the
stored access token fine (which is why the CLI worked).

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

* style: format helpers.test.ts with biome

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

---------

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

* fix(ui): stack code block lines when streamdown lineNumbers is off (#13412)

streamdown renders each Shiki token line as a bare inline span with no
newline text between non-empty lines, and only applies its block line
class when lineNumbers is on. With lineNumbers off (the desktop app's
config) every multi-line fenced block collapsed into one run-on line.
Make the direct line spans under code-block-body display: block in the
shared markdown.css; empty lines keep their height via their lone "\n"
child under white-space: pre.

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

* fix(desktop): work summary undercounts wall time when pre-tool thinking attaches to the answer (#13413)

* fix(desktop): anchor work summary duration on the answer row, not attached pre-tool reasoning

The collapsed 'Worked for Xs' row undercounted wall time whenever a turn's
assistant message contained thinking + tool_use with no narration text: the
canonical projection emitted the reasoning-only row after the tool row (both
stamped before the tool executed), the webview attached that row to the final
answer, and collapseCompletedWork used the answer's earliest attached
reasoning timestamp as the end anchor - excluding the entire tool execution
(e.g. 'Worked for 5s' for a turn with an 8s command).

- webview: end the work span at the answer row's own timestamp, clamped to
  the last collapsed row so a fallback answer bubble with a synthetic early
  timestamp cannot shrink the duration either
- sidecar: flush pending thinking before a tool_use row so rehydrated
  transcripts keep the live-stream order (thinking before its tool call) and
  pre-tool reasoning no longer rides on the next answer

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

* fix(desktop): keep interleaved thinking between the tool calls it separates

Address Greptile review: when one assistant message interleaves thinking
between multiple tool_use blocks, each reasoning segment now projects at its
own position (attached to a text row from its own segment when present,
otherwise as its own row) instead of merging into the first reasoning row,
which displayed later thinking before a tool call it actually followed.

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

---------

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

* fix(desktop): remove settings gear hover state while Account screen is open (#13408)

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

* fix(desktop): don't show "No sessions found" while session history is still loading (#13414)

* fix(desktop): don't show 'No sessions found' while session history is still loading

Replace the isLoadingHistory flag with hasLoadedHistory, set only once the
backend has actually answered a list_discovered_sessions request. The sidebar
and Sessions view now keep their loading state until that first definitive
response, so the empty-state copy can no longer appear while history is still
being fetched (or while a failed fetch is being retried).

Also retry a failed initial fetch on the 2s event cadence instead of stranding
the UI until the 12s periodic poll, which is what stretched the misleading
empty state to ~10 seconds after a webview reload when the websocket lost the
race with the page load.

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

* fix(desktop): stop history fast-retry from re-arming after hook unmount

A failed initial fetch that settles after the hook unmounted could schedule a
new retry timer after cleanup had already cleared the refs, leaving the
abandoned hook polling the backend every 2s. Guard scheduleRefresh with a
disposed ref set by the mount effect's cleanup.

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

---------

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

* Fix @ file mentions breaking on paths with spaces (#13391)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

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

* Fix import ordering in mentions test (biome organize imports)

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

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

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

* Normalize mention paths to posix separators for Windows

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

---------

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

* Fix code actions failing with "command not found" on VS Code 1.134 (#13402)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

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

* Fix import ordering in mentions test (biome organize imports)

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

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

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

* Normalize mention paths to posix separators for Windows

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

* Fix code actions failing with 'command not found' on VS Code 1.134

Code action commands carried arguments (expandedRange, diagnostics),
which routes them through VS Code's CommandsConverter cache. VS Code
1.134 disposes the cached entries before the clicked action executes,
so every lightbulb action failed with 'Actual command not found,
wanted to execute cline.addToChat'.

Drop the arguments so the command id is passed through directly, and
recover the context in the handler instead: getContextForCommand now
expands an empty selection by 3 surrounding lines (matching the old
provider behavior) and gathers document diagnostics intersecting the
range when none are passed explicitly.

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

* Scope gathered diagnostics to the selection/cursor

Match the old CodeActionContext.diagnostics behavior: only include
diagnostics intersecting the range the action was requested for, not
the surrounding lines the text gets expanded to.

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

---------

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

* Desktop: unify Plugins, MCP, and Skills into one Plugins hub with a dedicated Marketplace page (#13411)

* Unify desktop plugins, apps, MCP, and skills into one Plugins hub with a Browse directory mode

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

* Open the marketplace directory as a modal over the Plugins hub instead of swapping the page

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

* Rename directory to Marketplace: Browse Marketplace button, Marketplace modal title with icon, search placeholder

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

* Fix search input focus ring clipped by the Marketplace modal scroll container

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

* Address Greptile review: keep selected tag chip visible when its count drops to zero, and remount installed tab when a marketplace install completes after the modal closed

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

* Track marketplace modal mutation flag in a ref so a close click racing a queued render cannot skip the inventory remount

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

* Make Marketplace its own settings page under Customizations and restore Channels as a standalone page

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

* Remove icon from Marketplace page header for consistency with other settings pages

* Notify mounted inventory views when the marketplace invalidates the cache so late install completions refresh the Plugins hub

---------

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

* feat(desktop/ui): recommended and free model tiers in the composer model selector (#13410)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

---------

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

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK (#13415)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

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

* feat(desktop): recommended-feed badges and descriptions in provider settings (#13416)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* feat(desktop): recommended-feed badges and descriptions in provider settings

Review suggestion on #13410: the provider settings page has room for
more model detail than the composer's picker. The cline/cline-pass
provider cards now refresh their model list through
list_provider_models (the catalog snapshot deliberately skips the
recommended-feed overlay so the startup catalog fetch never blocks on
the feed) and render Recommended/Free tier badges plus feed tags (NEW)
next to the model name, with the model description underneath. The
refreshed list also surfaces the live entries instead of the bundled
snapshot.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

* fix(desktop): scope the settings featured model list to its provider and revision

The fetched featured list was unscoped component state: switching
between cline and cline-pass reused the component instance, so the
previous provider's models stayed visible while the new request was
pending (or forever, when it failed), and the retained copy shadowed
later provider.modelList updates — adding a second custom model
submitted the stale list as the complete configuration and dropped the
first addition.

The fetched list now only applies to the provider and modelList
revision it was fetched for (falling back to the catalog snapshot
otherwise and refetching on membership changes), and add-model submits
the union of the displayed and configured ids so an update can never
silently unconfigure existing entries.

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

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

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

* fix(ui): update packed-Tailwind smoke contract for the picker's max-h-64 (#13421)

The ui-publish smoke check pins a set of Tailwind candidates the packed
sources must emit; #13410 grew the SearchCombobox options list from
max-h-56 to max-h-64, so the publish run failed on the stale candidate.
All other pinned candidates verified against the current sources.

* feat(desktop): refresh app icons and branding (#13400)

* ci(vscode): upload E2E failure recordings from the right path (#13427)

The job sets working-directory: apps/vscode, but that default applies to run
steps only, not to `uses:` steps. Since #10961 moved the extension under apps/
and added that default, the artifact path has resolved against the repo root,
matched nothing, and every failing run logged "No files were found with the
provided path: test-results/playwright/" instead of uploading recordings.

Widen to test-results/ so Playwright's error-context snapshots ship alongside
the videos.

* fix(hooks): deliver tool hook contextModification to the model (#13297)

* fix(hooks): deliver tool hook contextModification to the model

On the next engine, a tool_call (PreToolUse) hook's contextModification
was parsed into HookControl.context and then silently dropped: the
runtime beforeTool/afterTool result contract had no channel for
injecting conversation context. Legacy consumed it (ToolExecutor /
ToolHookUtils pushed <hook_context> blocks into the next user turn), so
this was a regression of documented behavior.

- Add appendContext to AgentBeforeToolResult/AgentAfterToolResult.
- AgentRuntime collects appendContext across hooks during an
  iteration's tool executions and appends one <hook_context> user
  message after the tool results, keeping tool-result parts contiguous.
- Map HookControl.context into appendContext in both subprocess hook
  layers (skipped when the hook cancels, matching legacy, where the
  message doubled as the error).
- Truncate injected context at 50KB per hook output, matching legacy.
- Concatenate appendContext across merged hook layers.

tool_result (PostToolUse) hooks still run detached with stdout ignored;
making them blocking so their context can be collected is a follow-up.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): stamp tool identity on injected hook context blocks

Contexts are batched into one message after the tool results, and
parallel tool execution collects them in completion order, so position
alone cannot attribute a block to its tool call. Add tool_name and
tool_call_id attributes to each <hook_context> block.

* fix(hooks): sanitize hook context block markup

Attribute values (tool_name, tool_call_id) are stripped of quote/angle
characters and embedded </hook_context> closers in hook output are
neutralized, so neither provider-supplied ids nor hook text can corrupt
or spoof a block's stamped identity.

* fix(hooks): neutralize forged opening hook_context tags in hook output

The previous sanitization only neutralized closing tags, so hook output
could still open a forged <hook_context> block claiming another tool's
identity. Escape both opening and closing embedded tags with one rule.

* fix(hooks): hide injected hook context from user-facing transcripts

Stamp the injected hook-context user message with displayRole 'system'
(the compaction-summary convention) so it reaches the model but does
not render as a user bubble in live or replayed transcripts. Without
this, resuming a session showed the raw <hook_context> block as if the
user had typed it.

* fix(hooks): neutralize case-variant embedded hook_context tags

The tag-neutralization regex was case-sensitive, so hook output could
still smuggle a forged tag as <HOOK_CONTEXT>. Match case-insensitively.

* fix(vscode): map PreToolUse contextModification into runtime appendContext

The extension's hooks adapter bridged file hooks into the SDK runtime
but forwarded only cancel/errorMessage, so a PreToolUse hook's
contextModification never reached the model. Map it into the runtime's
appendContext channel; HookFactory already truncates it at 50KB.

* fix(vscode): hide hook-injected context from replayed transcripts

Live sessions never rendered the injected <hook_context> user message,
but session reload replayed it as a user bubble (and post-resume turns
kept doing so). Treat these messages as synthetic in the user-message
mapping: honor the displayRole 'system' stamp the runtime sets, with a
text-prefix guard for paths where metadata is unavailable. This also
keeps edit/regenerate ordinal mapping aligned with visible bubbles.

* fix(hooks): run file hooks through exactly one layer per host

The VS Code extension registered two independent hook execution layers:
its own hooks adapter (config.hooks) and the SDK core's file-hook
extension from the runtime bootstrap. When both discover the same hook
files, every hook executes twice per event — and with context injection
wired, each contextModification would be injected twice.

Add a 'hooks' runtime config extension kind (in the default set, so the
CLI keeps core file hooks unchanged) and gate the bootstrap's file-hook
extension on it. The extension excludes 'hooks' at session start, so
its adapter — which also provides the hook status UI and the
hooksEnabled setting — is its single execution path.

* fix(vscode): discover hooks from the session workspace, not only global state

Hook discovery read workspaceRoots from global state shared across
every Cline instance, so another window repointing it made workspace
hooks silently stop being discovered. With the extension's adapter now
the single hook execution layer, that meant no hooks at all.

HookFactory takes an optional sessionWorkspaceRoot and unions that
root's .clinerules/hooks into discovery (and into cwd resolution), fed
from the session config's cwd. Shared-state discovery still works, so
behavior in the single-window case is unchanged.

* fix(hooks): keep sanitized hook attribute values distinguishable

Replacing every markup delimiter with the same underscore could
collapse two tool call ids that differ only by such a character into
identical stamps. Escape each delimiter with a distinct token instead.

* fix(hooks): make hook attribute sanitization injective

Escaping the underscore itself turns the attribute escaping into a
uniquely decodable code, so no two distinct tool call ids can collapse
to the same sanitized stamp (previously an id containing a literal
escape token could collide with an id containing the delimiter).

* fix(vscode): reconstruct hook status rows when replaying transcripts

hook_status messages are emitted live but never persisted, so reloading
a session dropped every hook row. The injected <hook_context> blocks
carry the hook source and tool name, so the replay translator now
rebuilds a completed hook status row from each block. The injection is
also no longer treated as a user turn boundary, so the final turn's
completion retag is unaffected by it.

* fix(hooks): collect PostToolUse hook output and honor its control (#13298)

* fix(hooks): collect PostToolUse hook output and honor its control

tool_result (PostToolUse) hooks ran fire-and-forget with stdout
ignored, so their entire JSON output — contextModification and cancel —
was discarded. Legacy awaited PostToolUse, injected its
contextModification into the conversation, and honored cancel.

- Run tool_result hook commands blocking (same 120s default timeout as
  tool_call) in both the hook-config-file layer and the agent-hook
  subprocess layer.
- Map their output: cancel stops the run with the hook's error message
  as the reason; otherwise context is injected via afterTool
  appendContext.

This restores legacy blocking semantics: tool results now wait for
tool_result hooks, but only in sessions that have one configured.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): bound tool_result hook wait and isolate cancel reason

Address review findings:
- The agent-hook subprocess layer forwarded an unset timeoutMs
  unchanged, so a tool hook command that never exits would block the
  agent indefinitely. Default both tool_call and tool_result to the
  120s bound the hook-config-file layer already used.
- A cancelling hook's error message was folded into the same context
  field as other hooks' injectable context, so merging controls could
  leak unrelated hook context into the cancellation reason. Carry it as
  a separate cancelReason, and surface it as the stop reason for
  beforeTool cancels too.

* fix(hooks): prefer errorMessage as a cancelling hook's stop reason

When a cancelling hook returns both contextModification and
errorMessage, the context-first parse precedence made the injectable
context the cancel reason and discarded the actual error. Parse the two
fields separately: errorMessage wins as the cancel reason (matching
legacy), and a lone errorMessage still folds into injectable context
for non-cancelling hooks as before.

* fix(vscode): honor PostToolUse hook cancel and contextModification

The adapter awaited PostToolUse hooks but discarded their output
entirely. Map cancel to a stop control (with errorMessage as the
reason) and contextModification into the runtime appendContext channel,
matching the PreToolUse mapping and legacy semantics.

* fix(hooks): whitespace-only errorMessage no longer suppresses the cancel reason

A cancelling hook returning meaningful context alongside a blank
errorMessage lost both: the parsers selected the whitespace as the
reason and the result mappers trimmed it away. Require a non-blank
errorMessage before it wins, so context serves as the fallback reason.
Apply the same fallback in the extension adapter's stop mapping.

* fix(core): stop Windows CI worker crashes from the agenda spec watcher (#13428)

* fix(core): watch agenda task specs via the resolved long path

fs.watch on a path with 8.3 short components (e.g. C:\Users\RUNNER~1
temp dirs) trips a libuv assertion in fs-event.c on Windows and aborts
the whole process. Since the agenda task manager landed, every hub
server test spins up its spec watcher on such a path on hosted Windows
runners, killing the vitest worker and failing the sdk-test Windows job
on every branch. Resolve the specs dir with realpathSync.native before
watching so libuv only ever sees the long form.

* test(ui): stub ResizeObserver for @pierre/diffs in tool-diff tests

jsdom does not implement ResizeObserver, so every ToolFileDiff render
logged a ReferenceError from @pierre/diffs to stderr. Tests still
passed; this just silences the noise the same way the constructable
stylesheet shim does.

* fix(core): skip the agenda spec watcher when the dir does not resolve

Falling back to the raw path on realpath failure would reintroduce the
Windows short-path abort; log and go without the watcher instead.

* fix(vscode): honor the classic truncation range when migrating legacy tasks (#13419)

Classic Cline truncated long conversations by omitting an index range of
api_conversation_history from every API request (keep the first
user-assistant pair, drop everything through the range end, strip
orphaned tool_results from the first kept message). The range was
persisted on the history item while the full history stayed on disk.

legacyApiHistoryToSdkMessages ignored conversationHistoryDeletedRange
and converted the entire file, so resuming a migrated long task handed
the SDK an untruncated working context that could exceed the model's
context window by millions of tokens - every request failed with
'prompt is too long' and every compaction restarted from the full
history (#12996, confirmed by the reporter: the task was migrated from
an older version and broke after a restart, with each compaction
starting from ~3M tokens).

The migration now replays exactly what the classic extension sent:
slice out the deleted range and drop orphaned tool_results, mirroring
ContextManager.getTruncatedMessages (see origin/main). Malformed ranges
fall back to the full history (previous behavior).

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

* fix(vscode): show the diff edit view for multi-line edits in CRLF files (#13417)

The edit preview computed proposed content with an exact old_text match, but
the SDK executor normalizes old/new text to the file's own line endings before
matching (#12305) - reads strip CR, so models emit LF-only text even for CRLF
files. Any multi-line old_text in a CRLF file therefore failed the preview's
match: the diff edit view silently never opened while the executor applied the
edit. Single-line edits (no line break in old_text) were unaffected, which is
why the diff view appeared to trigger inconsistently.

Mirror the executor's EOL normalization (and its literal $-sequence insertion)
in the preview computation.

Fixes #13296

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

* fix(core): report truthful session status so desktop checkpoint restore stops wedging (#13418)

* fix(core): keep hub session status truthful across queue-drained turns

Queue-drained turns settle only through the event stream, but the hub
runtime host mistranslated their lifecycle in two ways:

- session.updated events carrying only a snapshot (persistence updates)
  defaulted the projected status to "running". When one trailed the
  final idle update after a turn, clients that track busy state from
  status events (the desktop sidecar's workspace restore gate) stayed
  busy forever. Use the snapshot's real status and emit nothing when
  neither source reports one.
- the per-run agent.done dedup was only reset by run.started, which the
  daemon-side queue drain never publishes, so a drained turn's done was
  swallowed as a duplicate of the previous turn's. Reset the dedup on
  session.pending_prompt_submitted, and suppress stale run.completed
  events that land inside a drained turn's window so they can neither
  emit a phantom done nor consume the drained turn's dedup slot.

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

* test(desktop): cover restore unlock after an event-settled queued turn

Exports the sidecar's core-session event handler so the queued-turn
lifecycle (busy via status events, cleared by the done agent event,
restore allowed afterwards) is testable end-to-end.

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

* fix(core): start interactive sessions without a prompt as idle

The runtime host reported every new session as "running" until its
first turn ended. Interactive hosts (the desktop app) start sessions
with no prompt and dispatch turns through separate send calls, so a
created-but-never-prompted session stayed "running" forever — wedging
clients that gate workspace operations (checkpoint restore, message
edit) on active turns.

Interactive no-prompt starts now begin idle, start emits the session's
actual status (resumed sessions no longer masquerade as running), and
markTurn* transitions keep tracking in-memory status for lazily
persisted sessions so the first turn still reports running -> idle.

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

* style: format hub-runtime-host test filter

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

* refactor: drop the drained-turn done bookkeeping, keep the minimal fix

The stuck restore is fully explained by the two status defects (fabricated
"running" from snapshot-only session.updated events, and never-prompted
interactive sessions reporting "running"). The done-dedup machinery for
queue-drained turns addressed a separate cosmetic gap (queued turns emit no
chat_done, pre-existing) and required fragile run-window heuristics, so it
is removed to keep this change reviewable. Sidecar test now settles the
queued turn through the status event, matching the shipped mechanism.

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

* docs(sdk): document the truthful session-status contract

---------

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

* chore(deps): update Langfuse packages and bump app versions (#13443)

* chore(deps): update Langfuse packages and bump app versions

Update @langfuse/otel to v5.10.1 and add @langfuse/vercel-ai-sdk v5.9.1 for improved observability with Vercel AI SDK.

Bump versions for @cline/code to 0.0.14 and @cline/ui to 0.2.0-next.6, updated via bun.lock.

Other Changes:
Added optional userId to AgentRuntimeConfig.
Propagated userId, sessionId, conversationId, runId, iteration, provider, and model context into AI SDK telemetry.
Added AI SDK 7 runtimeContext with explicit includeRuntimeContext.
Added stable OTEL_SERVICE_NAME=cline-sdk.
Added runtime metadata assertions in agent tests.

* add taskId

* Revert "add taskId"

This reverts commit f20d31d96d.

* docs: simplify Open Cline step in installing guide (#13405)

* docs: remove duplicate GLM-5.3 rows in ClinePass tables (#13449)

Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>

* chore(sdk): release v0.0.76

* chore(cli): release v3.0.56

* docs(cli): scope the v3.0.56 release notes to CLI-visible changes

* feat(desktop): interactive welcome hero graphic (#13399)

* feat(desktop): add interactive welcome hero

* feat(desktop): support composable welcome hero variants

* feat(desktop): reskin first-run onboarding (#13441)

* refactor: centralize client tool availability (#13451)

* chore(sdk): release v0.0.77

* docs(cli): drop the tasks tool from the v3.0.56 notes, it is desktop-only

* chore(vscode): prepare 4.1.11 release

* chore(desktop): release v0.0.15

* fix(vscode): remote config MCP settings (#13466)

* fix(vscode): enforce enterprise MCP controls on the Customize marketplace

The unified Customize marketplace replaced the old MCP marketplace
without carrying over enterprise remote-config enforcement: the catalog
RPC returned every MCP entry and installs were never policy-checked,
so orgs with mcpMarketplaceEnabled=false or an allowedMCPServers
allowlist saw (and could install) all marketplace MCP servers.

- Filter MCP entries out of getMarketplaceCatalog when the marketplace
  is disabled, and restrict entries to the allowlist when configured
  (matching entry id, display name, installed server name, or source
  repo URL, mirroring legacy GitHub-URL allowlist ids)
- Reject installMarketplaceEntry requests that violate the policy
- Map the published catalog's repo/homepage fields onto
  sourceUrl/homepageUrl so URL-based allowlists can match
- Update the enterprise MCP server controls docs

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

* refactor: simplify MCP marketplace policy enforcement

Fold the policy check into marketplace-helpers, drop the dedicated
test suite, and trim the docs edit to the strictly necessary line.

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

---------

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

* Treat an empty preserved capability list as unspecified when seeding tools (#13465)

* Treat an empty preserved capability list as unspecified when seeding tools

toSdkModelInfo guarded the tools seeding with a strict
preservedCapabilities === undefined check, but modelHasCapability —
the runtime's own reader — treats undefined AND length === 0 as
"unspecified". A custom OpenAI-Compatible model whose stored
capabilities field is a defined-but-empty array (a config carried over
from before the field existed, or one round-tripped through a boundary
that defaults it to []) skipped the seeding; the first boolean
projection to run afterwards (e.g. supportsReasoning) then populated
the array, the runtime gate read the non-empty, tool-less list as
authoritative, and every tool definition was silently dropped from the
session (#13463).

The guard now covers the empty array too, matching the reader's
unspecified semantics.

* test: satisfy the store's isModelInfo gate so the empty-capabilities case actually reaches knownModels

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

---------

Co-authored-by: yzxcj797 <yzxcj797@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>

* chore(vscode): prepare 4.1.12 release

* Add feature flags to the desktop app (#13289)

* Add feature flags to the app

* React to account updates

* Address comments

* use a per-app file

* fix: propagate Langfuse session telemetry (#13473)

* fix telemetry session propagation

* feat telemetry client version metadata

* fix(core): address Langfuse review feedback — hub client identity + delegated agent session grouping (#13475)

* fix(core): rebuild hub session client identity from request headers

Hub-backed sessions do not transport extensionContext (it is local-only),
so the daemon's runtime built traces without the clientName/clientVersion
metadata even though the hub client bakes X-CLIENT-TYPE / X-CLIENT-VERSION
into the session's provider headers. Reconstruct extensionContext.client
from those headers during local runtime bootstrap so hub-backed Langfuse
traces carry the same client identity as local runtimes, and the daemon's
header re-resolution stops clobbering the original X-CLIENT-TYPE.

* fix(core): propagate parent distinctId/sessionId to delegated agents

Delegated agents (spawned sub-agents, configured agents, teammates) were
built without distinctId and sessionId, so their Langfuse traces had no
userId or sessionId and did not group with the parent user or session.
Thread the host-resolved distinctId through RuntimeBuilderInput and the
root sessionId through the delegated-agent config provider, and copy both
onto the delegated AgentConfig.

---------

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

---------

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

* ci(vscode): make combined nightly manual-dispatch only

The PublishNightly environment gained required reviewers, so each cron
run parked on approval, held the workflow's concurrency group, and
silently cancelled every scheduled run queued behind it. 20 consecutive
scheduled nightlies died this way between 2026-07-31 and 2026-08-21;
the only nightlies that shipped in that window were manual dispatches.

Drop the cron rather than leave a trigger that cannot succeed unattended.

* feat(hub): add drain and upgrade commands with replay support (#13468)

* feat(hub): add drain and upgrade commands with replay support

* handles disconnection

* feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport

Completes the wiring the previous commits' primitives needed:
HubServerTransport gains isDraining(), hub.drain/hub.status/profile.get
command handling, and replayEventsAfter() (backed by the durable event
log), plus the sequence/sinceSequence wire types they depend on in
shared/hub.ts. run-queue-handlers.ts reads the active bot profile's
plugin roots when executing durable runs.

Also adds hub/profiles/: profile.json (identity/rules/plugins) ->
system prompt composition, --profile / CLINE_HUB_BOT_PROFILE
resolution, and the bundled cline-dad profile with its
cline_hub_support read-only diagnostics tool.

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

* Revert "feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport"

This reverts commit 6696d5d202.

* fix(hub): dedupe replayed events by eventId, not just sequence

HubEventLogStore.append() returns a new envelope stamped with a
sequence rather than mutating the input, so a pending approval
re-issued sequence-less by subscribe() (it predates any durable-log
append) and its later sequence-stamped copy from the durable log are
two different objects carrying the same eventId. The replay-then-live
buffer in browser-websocket.ts only deduped by sequence, so the
sequence-less copy's guard never tripped and it was delivered a second
time when the buffer flushed after replay.

Track delivered eventIds alongside the sequence cursor; eventId
survives the append/stamp round-trip unchanged, so this dedupes the
exact-same logical event regardless of which copy arrives first.

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

* fix(hub): wire drain, durable event log, and run queue into the live transport

CI on this branch failed bun run build:sdk: browser-websocket.ts,
client/index.ts, and hub-websocket-server.ts (already on this branch)
reference sequence/sinceSequence, HubServerTransport.isDraining(), and
the "hub.drain" command — but the commit that reverted bot profiles
out of this branch also reverted this wiring, since it shared a commit
with the profiles work. That wiring is a hub concern, not a
bot-profiles one; split it back out.

- shared/hub.ts: sequence/sinceSequence types, run.enqueue/run.list/
  hub.drain/hub.status/stream.replay capability, command, and event
  names. profile.get intentionally excluded — stays bot-profiles-only.
- context.ts: isDraining() on HubTransportContext. botProfile field
  intentionally excluded.
- hub-server-transport.ts: eventLog/runQueue fields and start/stop
  lifecycle, publish() appends to the durable log, handleCommand cases
  for run.enqueue/run.list/hub.drain/hub.status, drain-refusal check,
  replayEventsAfter()/lastEventSequence(). startBotProfile()/
  startHubSupportTool() and the profile.get case intentionally
  excluded.
- run-queue-handlers.ts: added without handleProfileGet (needs
  ctx.botProfile, which doesn't exist here).
- hub-upgrades.test.ts: added without its two bot-profile-injection
  tests (they need a resolved bot profile to assert against).

Verified bun run build:sdk exits 0 (the exact CI command) and
bunx vitest run src/hub passes (311/312; the one failure is the
same pre-existing environment-timing flake already present before
this change).

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

* fix(hub): export instance-lock, event-log, and run-queue from the hub barrel

These landed as internal modules only; hub-server-transport.ts and
hub-websocket-server.ts import them by direct path, but nothing
re-exported them from the public @cline/core/hub surface the way
sibling discovery/server modules already are.

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

* fix(hub): wire the instance lock into the daemon entry point

The singleton lock (discovery/instance-lock.ts) and its consumption in
startHubWebSocketServer/ensureHubWebSocketServer were already on this
branch, but the daemon entry point's own half was not: retrying a bind
when a retiring predecessor still holds the lock, and exiting with a
distinct code (3) instead of the generic fatal path when a live Hub
already owns the data directory. Without this, a daemon racing a
retiring predecessor could fail outright instead of waiting the lock
out, and losing the singleton race looked identical to a crash.

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

* fix(hub): address drain/upgrade review findings (#13478)

- cline hub upgrade: check idleness at least once (--wait 0 works), reject
  non-numeric --wait, and un-drain on every abort path so an aborted
  upgrade can never leave the hub refusing new work
- add cline hub drain --off and the off query param to requestHubDrain so
  POST /drain?off is reachable from shipped code
- HubEventLogStore/HubRunQueue: WAL journal mode + busy_timeout, and stamp
  sequences from lastInsertRowid instead of SELECT MAX(sequence)
- HubInstanceLock.acquire: degrade to an unheld lock when SQLite is
  unavailable instead of refusing hub startup; only BUSY/LOCKED still
  raises HubLockHeldError
- ensureHubWebSocketServer: retire an unusable discovered hub through the
  shared retireDiscoveredHub (busy hubs are attached to, drain precedes
  shutdown, discovery cleared only when the hub actually retired)
- replay adapter: advance the cursor past eventId-deduped events, cap
  replay pages, stop when the cursor stalls, and drop the dedupe set after
  the buffered flush so it cannot grow for the socket lifetime

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

* fix(hub): derive the singleton e2e challenger cwd portably

The challenger's working directory was derived by round-tripping the
discovery path through a file: URL and stripping the last pathname
segment. On Windows that yields a POSIX-style '/C:/...' path, which is
not a valid spawn cwd, so the spawn fails ENOENT before the singleton
lock is ever contested and the Windows SDK test job goes red.

The data dir is simply the discovery file's parent: use dirname().

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop stored capability lists from silently revoking tool calling for custom models (#13476)

* fix(core): seed tools capability when custom model capabilities are synthesized from boolean flags

For a models.json entry with no explicit capabilities list, toStoredModelInfo
synthesized a capability array purely from boolean convenience flags (e.g.
supportsReasoning: true -> ["reasoning"]). modelSupportsToolCalling fails open
only for a missing or empty list, so the synthesized non-empty list read as an
authoritative denial and silently stripped every tool definition from requests
to custom OpenAI-compatible models (#13463).

Seed "tools" whenever the list was not explicitly authored and the boolean
projections made it non-empty, preserving the fail-open contract. Explicitly
authored capability lists remain authoritative and can still disable tools.

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

* test(core): cover stale catalog capability overrides

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

* fix: treat stored capability lists as non-authoritative for tool calling

The hasExplicitCapabilities guard still let two producers of tool-less
lists through:

- The VS Code legacy-override migration (legacyModelInfoToOverrides)
  persists explicit partial lists like ["prompt-cache"] into models.json
  for custom OpenAI-compatible models, which then read as an authoritative
  "cannot call tools" and drop every tool - same symptom as #13463.
- Any hand- or UI-authored partial list on a non-catalog model.

Stored entries and user-authored provider metadata have no way to declare
"cannot call tools" (there is no supportsTools field, and every writer
that authors a full list includes "tools"), so seed "tools" into any
non-empty list for a language model. Only generated catalog capabilities
remain authoritative - a genuine no-tools catalog model stays that way -
and non-language models (e.g. image generation) never gain a tools claim.

Also make legacyModelInfoToOverrides write "tools" into the arrays it
fabricates, matching the providers.json migration, so models.json stops
being poisoned for older readers.

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

---------

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

* chore(vscode): prepare 4.1.13 release

* chore(sdk): release v0.0.78

* chore(cli): release v3.0.57

* fix(core): run hub e2e files serially so daemon timing budgets survive CI contention

singleton.e2e.test.ts (added in #13468) spawns real daemons and runs for
~15s. Vitest's default file parallelism let it run alongside
shutdown.e2e.test.ts, whose assertions are wall-clock bound: discovery
within 10s, exit within 5s, and a 2s shutdown watchdog. On the 2-core
windows-latest runner that contention alone broke those budgets, failing
the shutdown test two different ways across runs — once never observing
discovery, once with the daemon forced to exit before its HTTP 202
flushed (socket hang up). The test passed on Windows before #13468 and
has failed every SDK publish run since.

* chore(desktop): release v0.0.16

* test(sdk): give windows-sensitive suites realistic timeouts

Four consecutive SDK publish runs failed on windows-latest, each on a
different test, all of them plain timeouts: two @cline/shared SQLite
tests at the 5s vitest default, core's bash executor at 10s, and the hub
singleton endpoint test at 10s. The 2-core Windows runner spawns forks
and takes SQLite locks slowly enough to blow those budgets under load.

These timeouts guard against hangs; they are not timing assertions (the
one suite that does assert elapsed time, shutdown.e2e, was fixed by
removing file-level parallelism instead). Raise core to 20s and give
@cline/shared an explicit 15s in place of the inherited 5s default.

* fix(telemetry): emit task.completed from every session teardown path (#13489)

The task.completed fallback lived only inside shutdownSession, but
stopSession/dispose route interactive sessions with a terminal reported
status through releaseSessionRuntime, which never emitted. Truthful
session-status reporting (shipped in 4.1.11) re-routed a large share of
interactive stops onto that branch and silently dropped the event.

Route the emission through a single choke point,
emitTaskCompletedOnTeardown, called from both shutdownSession and
releaseSessionRuntime. The completion criterion no longer reads
session.status: interactive sessions use the recorded final-turn
outcome (lastInteractiveTurnFinishReason), non-interactive sessions
keep the existing input.status === "completed" logic. A new
taskCompletedEmitted flag (also set by the submit_and_exit observer)
enforces exactly one task.completed per session. failSession now
records the errored final turn so a stale "completed" from an earlier
turn can never leak into the teardown emission. Telemetry only; no
user-facing behavior changes.

* chore(vscode): release v4.1.14

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on (#13498)

* fix(vscode): honor MCP auto-approve settings for SDK tool calls

The SDK extension required both the global 'Use MCP servers' auto-approve
toggle AND each tool's per-tool autoApprove flag before silently approving
an MCP call, while the legacy extension treated them as either/or. Restore
the legacy OR semantics so toggling MCP auto-approve works again.

Also key toolPolicies by the registered SDK tool name (via
defaultMcpToolNameTransform, now exported from @cline/core) instead of raw
server__tool. Servers whose names contain sanitized characters (e.g.
marketplace names like github.com/user/repo) or exceed 64 chars produced
policy keys that never matched the registered tool, so those MCP tools ran
without any approval gate; the live auto-approve lookup now re-applies the
transform instead of string-splitting the name.

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

* Revert "fix(vscode): honor MCP auto-approve settings for SDK tool calls"

This reverts commit 86c568fbba.

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on

The SDK extension only auto-approved an MCP call when the global 'Use MCP
servers' auto-approve toggle AND that tool's per-tool autoApprove flag were
both set, so toggling MCP auto-approve appeared to do nothing and users had
to opt in each tool individually. The toggle alone now governs all MCP
tools; the per-tool flag is no longer consulted.

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

---------

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

* chore(vscode): release v4.1.15

* fix(cli): remove the $4.99 ClinePass promo copy (#13514)

The $4.99 first-month promo is ending, so the CLI's first-launch "Try ClinePass" dialog should no longer advertise it. Also drops the leftover CLI_PROMO_CODE plumbing, which has been an empty string since the promo-code flow was removed.

* fix(vscode): resolve hook workspace identity from the window, not shared global state (#13352)

* fix(vscode): resolve hook workspace identity from the window, not shared global state

Hook discovery, hook cwd selection, and the workspaceRoots metadata passed
to hook scripts all read the workspaceRoots/primaryRootIndex global state
keys. Global state lives in ~/.cline and is shared by every Cline instance
(all VS Code windows, the CLI, the JetBrains plugin), and nothing writes
these keys anymore, so hooks resolved against whatever project some other
or older instance last recorded. With a second window open on another
project, a workspace's .clinerules/hooks scripts were never discovered.

Resolve workspace roots via a single guarded helper backed by
HostProvider.workspace.getWorkspacePaths() (in-process, window-scoped,
same as refreshHooks): blank paths are filtered, a host-bridge failure
degrades to no workspace roots instead of silently disabling global hooks
or skipping blocking PreToolUse guards, and one resolution is threaded
through hooks-dir discovery, cache misses, cwd selection, and hook input
metadata so they can't disagree (previously up to four host lookups per
hook execution — real gRPC round trips in the standalone host). Roots and
hooks dirs are matched on whole path segments with the longest root
winning, so prefix-sharing or nested workspace roots resolve to the right
project. The adapter creates the runner once per event and skips no-op
runners, making creation the single resolution point; the separate
hasHook/getHookInfo checks are removed. The dead workspaceRoots and
primaryRootIndex state keys are dropped, and the four hand-rolled
HostProvider.workspace test stubs are consolidated into one shared
helper.

* test(vscode): add e2e coverage for workspace-scoped hook discovery

Boots real VS Code with the packaged extension against the workspace
fixture, sends a prompt, and asserts the fixture's UserPromptSubmit hook
was discovered from the open window's workspace, executed with that
workspace root as its cwd, and received the same root in its
workspaceRoots input — the end-to-end contract the hook workspace
identity fix establishes.

* test(vscode): isolate the e2e hook fixture from the shared workspace

The UserPromptSubmit fixture hook lived in the shared e2e workspace, so
every prompt-sending spec executed it (hooksEnabled defaults to true) —
and its cold PowerShell spawn on Windows pushed chat.test.ts past the
5s expect timeout. hooks.test.ts now overrides workspaceDir to a
dedicated workspace-hooks fixture, so only the hooks spec pays the hook
spawn.

* fix(hub): cap hub-events db size so it can't fill the disk (#13516)

* fix(hub): cap hub-events db size so it can't fill the disk

Row/time retention alone didn't bound disk usage: envelopes carrying
full session snapshots reach hundreds of KB each, so retained rows
could total tens of GB, sweeps only ran hourly, and DELETE never
shrinks a SQLite file. Enforce a 64 MiB size budget in prune() (oldest
rows first, VACUUM to return the space), and also prune after every
16 MiB appended so bursts can't outrun the hourly timer.

Fixes #13505

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

* fix(hub): tolerate VACUUM failure on a full disk

VACUUM needs scratch space and can fail in exactly the state a
ballooned event log causes. The byte-budget deletes already bound live
data, so swallow the error and let the next sweep retry the reclaim
instead of aborting startup pruning and disabling the durable log.

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

* fix(hub): count the size budget in UTF-8 bytes, not characters

envelopeJson.length (UTF-16 units) and SQLite LENGTH() (characters)
undercount multibyte text by up to 3x, which could leave a CJK-heavy
log settled above budget and re-running VACUUM every sweep. Use
Buffer.byteLength and LENGTH(CAST(... AS BLOB)) instead.

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

---------

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

* fix(sdk): carry root overrides into the Node smoke-test sandbox (#13517)

ci-node-smoke.ts installs the packed SDK tarballs with a plain npm
install in a fresh temp dir, where the repo root package.json overrides
do not apply. When @sap-cloud-sdk 4.9.0 shipped (2026-08-24) it broke
@sap-ai-sdk/ai-api 2.14.0 (via @jerome-benoit/sap-ai-provider in
@cline/llms) with ERR_PACKAGE_PATH_NOT_EXPORTED, failing the smoke step
on every PR even though the root already pins @sap-cloud-sdk/* to 4.6.0.

Copy the root overrides block into the generated sandbox package.json
so the smoke install resolves the same pinned versions as the repo and
future third-party releases cannot break it independently.

* chore(sdk): release v0.0.79

* fix(vscode): don't steal last-used provider from ClinePass on credential refresh (#13520)

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

* fix(hub): flush the /shutdown 202 before daemon teardown

The /shutdown handler queued teardown on a microtask, which runs before
the event loop's write phase, so the daemon could process.exit() before
the accepted 202 was handed to the socket. Unix masked it (uv_try_write
lands small loopback writes synchronously); Windows has no such fast
path and lost the race regularly — the recurring shutdown.e2e.test.ts
'socket hang up' failures on windows-latest. Start teardown from the
response's write callback instead, with an idempotent 1s fallback so a
client that vanishes mid-write cannot strand the daemon, and send
Connection: close so the client gets a FIN rather than an abort.

Since the flakiness this compensated for is fixed at the source, restore
maxWorkers: 2 for the Windows core suite (serializing it cost ~3 min of
CI per run), and raise the e2e daemon discovery hang guard 10s→30s —
it guards against hangs, not runner speed.

* chore(cli): release v3.0.58

* fix(core): prevent search_codebase from crashing the process on giant single-line files (#13525)

* fix(core): prevent search_codebase from crashing the process on giant single-line files

searchWithRipgrep buffered all of rg's --json stdout into one string. Each
JSON event embeds the full text of the matched line (--max-columns is
ignored in JSON mode), so searching a directory of serialized trace dumps
(single-line multi-hundred-MB JSON files) accumulated gigabytes of stdout
until string concatenation threw RangeError: Out of memory inside the
stream data handler. That throw is outside the tool's try/catch, so it
escalated to an uncaughtException and killed the CLI/hub daemon.

Parse rg's JSON events incrementally line by line, drop events larger
than 256KB, truncate matched/context lines to MAX_LINE_CHARS, and stop
reading once maxResults is reached. The fallback regex scan now skips
files larger than 10MB (reporting the skip count) and truncates its
context lines the same way.

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

* simplify search_codebase crash fix to a minimal diff

Replace the incremental JSON-event parser with three small guards: stop
buffering rg stdout past 10MB, drop the trailing partial event before
parsing, and slice fallback context lines to MAX_LINE_CHARS. Drops the
fallback file-size skip and skip-count reporting.

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

---------

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

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag (#13522)

* chore(vscode): remove per-tool MCP auto-approve checkboxes from webview

MCP auto-approval is now governed solely by the global 'Use MCP servers'
toggle; the SDK approval path (shared with the CLI and desktop app) has no
per-tool granularity, so the per-tool and 'Auto-approve all tools'
checkboxes were no-ops that implied control that no longer exists. Remove
them from the MCP settings view and chat tool rows. The autoApprove arrays
in cline_mcp_settings.json and the toggleToolAutoApprove RPC are left
intact for the legacy extension.

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

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag

Keep the checkbox components, handlers, and RPC plumbing intact but gate
rendering behind SHOW_MCP_PER_TOOL_AUTO_APPROVE=false: the SDK approval
path (shared with the CLI and desktop app) is all-or-nothing via the
global 'Use MCP servers' toggle, so the per-tool checkboxes were no-ops.
Flip the flag back on if the SDK gains per-tool approval granularity.

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

---------

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

* fix(tools): create new files with the platform-native line ending (#13521)

* fix(tools): use platform-native EOL for new files and preserve CRLF in apply_patch updates

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

* simplify to the minimal new-file EOL fix

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

* extract shared normalizeNewFileLineEndings helper

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

---------

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

* Add suggested schedule templates to the desktop Schedules page (#13529)

* Add suggested schedule templates to desktop Schedules page

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

* Fix unreadable selected text in inputs caused by selection utility conflict

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

* Restyle Suggested section label as small gray uppercase

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

* Hide suggested schedule cards that match an existing schedule name

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

---------

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

* Disable the agent todo tool and hide the Agenda UI in the desktop app (#13530)

* remove todo tool and Agenda UI, keep schedule-only tasks tool

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

* chore: biome formatting fixes

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

* restore agenda backend; disable todo kind behind a flag instead of deleting

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

* keep agenda automation pump idle while the todo tool is disabled

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

* remove todo tool and Agenda UI altogether (revert the disable-flag hybrid)

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

* restore all agenda code to main state

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

* disable agent todo tool and hide Agenda UI behind flags

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

---------

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

* Add Desktop App and Cloud Platform to bug report issue template (#13532)

* Add Desktop App and Cloud Platform to bug report surfaces

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

* Rename Surface Diagnostics field to Diagnostics

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

---------

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

* desktop: sidebar navigation cleanup with New/Schedule/Customize rows and dialog-based search (#13533)

* desktop: clean up sidebar navigation chrome

- Give New Task its own full-width labeled row below the logo row
  instead of an ambiguous icon next to the agenda toggle
- Wire the New Task row to the home action so starting a new task
  clearly takes you home (the logo still works as a fallback)
- Swap back/forward chevrons for browser-style arrow icons and
  bump their size

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

* desktop: sidebar New/Schedule/Customize rows and always-visible search

- Stack New (plus icon), Schedule, and Customize as full-width labeled
  rows below the logo; whole row highlights on hover via sidebarItem
- New starts a fresh task (home), Schedule opens Settings > Schedules,
  Customize opens the Customizations sections (Plugins first)
- Show the session search bar permanently above the sessions list
  instead of hiding it behind a search icon toggle

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

* desktop: move session search into a dialog behind a logo-row icon

- Replace the inline sidebar search bar with a search icon in the
  logo row that opens a cmdk command dialog listing sessions
- Selecting a result opens that session and closes the dialog
- Remove the agenda/tasks toggle the icon replaces, along with the
  now-unreachable sidebar Agenda panel (the welcome screen still
  surfaces agenda tasks)

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

* desktop: load full session history when the search dialog opens

Addresses Greptile review on #13533: the dialog only searched the
currently loaded history batch, so older unloaded sessions could not
be found. Opening search now kicks off loadAllSessions() (the hook's
purpose-built global-search loader), and the empty state reads
'Searching older sessions...' while more history is streaming in.

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

---------

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

* Hide Channels and Agents sections from desktop app sidebar (#13527)

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

* Desktop app: organize sidebar sessions into Pinned, Scheduled, and Tasks sections (#13528)

* Add Pinned/Scheduled/Tasks categories to desktop app sidebar

Replace the Schedules and Favorites filter-menu options with visible
collapsible category sections in the session sidebar, and rename the
Favorite action to Pin across the sidebar and sessions view.

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

* Grow full history window when Tasks show-more outpaces loaded tasks

loadMoreSessions treats its argument as a limit on all sessions, but the
Tasks show-more count only tracks Task rows, so once pinned/scheduled
rows pushed the loaded total past the requested count the call no-oped
and clicks went dead. Grow the whole history window via
loadOlderSessions instead, and only when the loaded tasks cannot fill
the next page. Addresses Greptile review on #13528.

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

* Auto-fill the Tasks page instead of fetching once per show-more click

A single 50-session window growth can consist entirely of pinned or
scheduled sessions, leaving a show-more click with no visible Tasks
progress. Replace the one-shot fetch with a page-fill effect that keeps
growing the history window until the requested Tasks page fills or
history runs out. Addresses the follow-up Greptile review on #13528.

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

* Halt page-fill retries after a failed history fetch

A failed fetch leaves the task count and has-more state unchanged,
which are exactly the conditions the page-fill effect fires on, so one
failing request would retry and re-toast forever. Halt the effect after
a failure and let the next explicit show-more click retry. Addresses
the third Greptile review on #13528.

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

---------

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

* Redesign desktop Model Providers page and split voice input into its own settings page (#13531)

* Redesign desktop Model Providers page and split voice input into its own settings page

- Group providers into Connected / Popular / All with auth-kind hints and
  connection status instead of per-row enable toggles
- Show browser sign-in (not an API key field) for OAuth providers, with a
  collapsed manual-key escape hatch where supported, plus explicit
  Connect / Disconnect / Sign out actions
- Move voice input to a dedicated Settings > Voice page that only offers
  connected transcription-capable providers, preselects a default model
  (streaming preferred), and stays disabled in the sidebar until a
  provider is connected

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

* Show native tooltip on the disabled Voice settings nav item

Disabled buttons drop pointer events, so the 'connect a model provider'
hint moves to a wrapping span for the browser tooltip to render.

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

* Drop letter avatars and gray provider ids from provider rows and voice chips

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

* Drop model counts from provider list rows

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

* Rename provider Connected status to Configured and drop the green styling

A settings entry is configuration, not a live connection; neutral gray
text avoids implying an active link, since the user still picks which
configured provider to use per chat.

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

* Resync provider catalog from disk when a settings save fails

Connect/disconnect/credential edits update the list optimistically; a
failed save now reloads the catalog instead of leaving the optimistic
state (and the view's module cache) claiming a configuration that was
never persisted.

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

* Rename oauthProvider test fixture to dodge CodeQL name heuristic

CodeQL's clear-text-storage query flags any identifier matching 'oauth'
as a credential source and traced the fixture's provider id into the
favorite-models localStorage write, which stores only provider/model id
strings. Renaming the fixture removes the false-positive source.

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

* Guard catalog reloads against races and resync detail drafts on failed saves

Optimistic provider mutations now bump a generation that discards any
in-flight catalog response, so a failed-save recovery reload can't
overwrite a newer edit with an older disk snapshot. The recovery also
remounts the provider detail panel via a reset token so its local field
drafts reflect the reloaded on-disk state instead of unpersisted edits
or an optimistically cleared disconnect.

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

* Fix failed-save recovery ordering and retry superseded reloads

Remount the provider detail only after the authoritative catalog reload
lands, so its drafts re-seed from disk state rather than the optimistic
values that failed to persist. When a concurrent edit supersedes the
recovery's in-flight response, retry the reload (bounded) instead of
dropping it, since that edit performs no reload of its own.

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

---------

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

* feat(desktop): Customize hub, sidebar overhaul, and settings polish (#13538)

* feat(desktop): merge customization pages into a Customize hub with inline marketplace

Replaces the Plugins page and the dedicated Marketplace page with a single
Customize hub. Tabs: Skills, MCP, Plugins, Rules, Hooks, Tools, each with
live counts. Tabs backed by a marketplace catalog render the installed
items followed by an inline browsable Browse section (CLI-hub style), so
installing from the catalog immediately reflects in Installed above.

- Installed cards restyled to mirror the browse-card anatomy: bg-card p-4
  containers, absolute top-right xs Uninstall matching Install, truncating
  semibold titles, primary-tinted icons, real Badge components instead of
  ad-hoc bordered spans, un-indented line-clamped descriptions
- Rules/Hooks/Tools rows brought into the same card language; redundant
  intro paragraphs (duplicating the page description) removed; Tools group
  headers match the Installed header style with counts
- Marketplace section header renamed to Browse; duplicate 'N results' row
  removed (the header count is the single source)
- MCP embedded view now shows the full marketplace instead of
  installed-only

* feat(desktop): overhaul sidebar sessions and navigation

Sessions list:
- Sort toggle removed; sessions are always grouped by project, with pinned
  sessions leading each group (both subsets ordered by recency). The
  Pinned/Scheduled/Tasks category sections and their time-mode paging
  machinery (page-fill effect included) are deleted
- Scheduled sessions get an inline clock icon next to the pin position;
  pin + clock render together when both apply, and the running/unread
  status dot now coexists with them
- One font size (text-sm) across the list: titles, timestamps, project
  headers, show-more buttons, empty states. sidebarText needed !text-sm
  because the default button size's text-base wins the twMerge conflict
- Gradient fade under the Sessions header once the list scrolls, so rows
  fade out instead of hard-clipping
- The session-detail hover card is controlled from the sidebar and closes
  on scroll (Radix receives no pointer events while scrolling, so it used
  to float over moving content)
- Sidebar min resize width raised 224->260px; the per-project show-more
  label truncates so its nowrap text can't force rows to overflow and clip
  timestamps at narrow widths

Navigation:
- Customize replaces the Plugins/Marketplace/Hooks/Rules/Tools sidebar
  entries; Schedules and Customize are hidden from the expanded settings
  nav (their top rows cover them) but stay reachable when collapsed
- The settings gear always opens General instead of resuming the last
  section; the Account no-op hover special case is gone
- The New row highlights (aria-current) while the fresh not-yet-started
  task page is showing and hands off to the session row once the task
  starts; hitting New also focuses the prompt input via a window-event
  signal (lib/prompt-input-focus.ts) since the sidebar and composer sit in
  distant subtrees
- Fixed the xs button size collapsing any icon-bearing button to 12x12
  (leftover has-[>svg]:size-3 from when xs was a micro button) — this was
  why Uninstall buttons rendered broken next to Install

* feat(desktop): polish settings pages and chat composer

Models page:
- The provider detail panel is always open: no X button, no empty
  no-selection state. It defaults to the first connected provider (falling
  back to the first in the catalog), which also removes the layout shift
  that happened when the page swapped between full-width and panel
  variants on selection
- Fixed the list pane becoming unscrollable while the panel was open:
  grid items default to min-size auto, so the pane grew past its track
  inside the overflow-hidden grid and its ScrollArea had nothing to
  scroll; wrapped it in a min-h-0 min-w-0 cell
- Add Provider opens a Dialog instead of swapping the page
  (AddProviderContent gained a dialog variant that renders only the form)
- Embedded inputs (provider search, model search, detail fields) share one
  EMBEDDED_INPUT_CLASS stripping the Input component's own border/dark bg
  tint/shadow/ring, which rendered as a mismatched inner box; the model
  search box uses the same h-9/px-3 frame as the provider search
- Model list flows with the page instead of a max-h capped inner scroller

Other pages:
- Account uses the shared PageFrame/PageHeader: left-aligned, text-3xl
  title, Sign Out in the header actions slot
- Desktop notifications is one General section: header row plus the
  Event/Notify/Sound matrix nested in a card, so its rows no longer read
  as top-level peers of Dark mode; 'Available in the desktop app' label
  removed
- Schedule page retitled from Schedules with a real description; Customize
  description rewritten

Chat composer:
- The voice dictation button only renders once a voice model is
  configured (Settings -> Voice); the unconfigured deep-link state is
  gone (prop type kept for an easy restore)

* chore(desktop): release v0.0.17

* fix(desktop): unblock sdk-test lint on the voice-input model picker (#13553)

The model picker renders a radiogroup of styled buttons with role=radio
and aria-checked; biome's useSemanticElements flags the role as an
error, which fails the sdk-test Quality Checks lint for every PR
touching sdk/ or apps/ paths. Suppress with a justification — switching
to input type=radio needs a restyle and belongs to the desktop settings
work.

* fix(vscode): include rich workspace metadata in system prompt (#13518)

* capture richer workspace information for vs code extension

* fix(shared): redact credentials from workspace remotes

* fix(shared): avoid regex backtracking in remote redaction

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>

* Hide task costs on vscode when ClinePass is selected (#13515)

* fix: stop showing cost estimates for subscription-billed providers (#13552)

* fix(vscode): stop showing cost estimates for subscription-billed providers

Providers whose usage is covered by a flat-rate subscription (ChatGPT
Plus/Pro via openai-codex, ClinePass) are marked with
metadata.usageCostDisplay = "subscription" in the SDK, and the CLI
already suppresses dollar figures for them. The VS Code host collapsed
that value into "show" before it reached the webview, so the task
header and model pricing rows rendered API-rate cost estimates that
users read as real charges on top of their subscription.

Pass all three usageCostDisplay values ("show" | "hide" |
"subscription") through the catalog listing and render cost only when
the value is "show", matching the CLI's shouldShowCliUsageCost
policy.

* feat(llms): mark Claude Code as a subscription-billed provider

Claude Code is typically authenticated with a Claude Pro/Max
subscription, but its models reuse Anthropic API pricing metadata, so
Cline rendered per-token prices and API-rate cost estimates for usage
that is covered by the subscription. Set usageCostDisplay =
"subscription" on the provider (picked up by the CLI and the VS Code
webview) and suppress the price rows in the Claude Code settings card.

The Claude Code CLI can also run on API-key billing, where a real cost
exists; the provider cannot distinguish the two, so we prefer showing
no number over a misleading one.

* fix(vscode): suppress cost display until provider listings load

While the ListProviders request is in flight (or after it fails), the
usage-cost hook had no listing to consult and fell back to "show",
flashing the API-rate estimate at subscription users on every chat-view
mount — the exact display the previous commit removes. Return
"unknown" whenever listings are absent; consumers already render cost
only for "show", so they suppress it during that window with no
changes. Briefly hiding a real cost is harmless, briefly showing a fake
charge is not.

* fix(desktop): reconcile voice settings after main sync

* test(llms): allow experimental ElevenLabs models

* fix(sdk): preserve canonical media model behavior

* feat(desktop): customize macOS DMG install window (#13563)

* feat(desktop): add Retina DMG background tooling

* feat(desktop): customize the macOS DMG layout

* ci(desktop): validate DMG background assets

* fix(desktop): adjust DMG Applications icon position

* ci(desktop): drop redundant DMG artwork validation from publish workflow

Tauri's beforeBuildCommand already runs dmg:background (with its own
validation) at the start of the build/sign/notarize step, and the
release/beta config overlays do not override the build section, so this
step duplicated work the publish job performs anyway. PR-time coverage
lives in desktop-test.yml.

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>

* feat(desktop): sidebar time view, Customize/Marketplace split, and schedule page UX (#13570)

* feat(desktop): split Customize into Installed and Marketplace pages

The Customize hub previously embedded a Browse section inside every tab
that had a catalog. That inlining made each tab long and buried the
catalog. Customize is now the installed inventory only (skills, MCP,
plugins, rules, hooks, tools tabs pass marketplaceVariant="installed"
to the embedded MarketplaceView; McpServersContent grew the same prop),
with an outline Marketplace button in the header.

Browsing moved to a dedicated Marketplace settings section that renders
the previously dead "directory" variant of MarketplaceView: one list
across all catalog types with type-filter chips, wrapping tag chips,
and light rules separating the filter tiers from each other and from
the results. The Clear control now renders inline at the end of the tag
row only while a tag is active, the Updated date is gone, and the
header hosts an Installed button mirroring the one on the Customize
page. Directory subheader copy: "A curated set of plugins, MCP
servers, and skills from the Cline community."

Tag and type chips wrap to new lines instead of scrolling
horizontally.

* feat(desktop): sidebar time view with sections, sort toggle, and scheduled detection

Restores the time-sorted session list as the default sidebar view, with
collapsible Pinned / Scheduled / Tasks sections (headers appear only
once something is pinned or scheduled) and the page-fill effect that
grows the fetched history window until a Show-more click makes visible
progress. Project grouping stays as the alternate mode behind a
one-click sort toggle whose icon reflects the active mode — the old
dropdown cost an extra click for a two-option choice.

Scheduled sessions are detected two ways: the hub-schedule origin
trigger in session metadata, plus a fallback that asks the hub which
session ids belong to schedule executions (list_routine_schedules,
fetched on mount and every two minutes, merged into a rolling set).
The fallback matters because locally executed scheduled runs do not
reliably stamp the trigger into session metadata — a real scheduled
session created today carried only {mode:"user"} provenance. The
scheduled clock icon now leads the row, left of the title; pin and
timestamp stay on the right.

The initial visible page grows from 10 to 30 rows so a tall sidebar
fills instead of stranding a stub of rows over empty space (history
fetches already start at 50).

The expanded sidebar's Customize row now hosts indented Installed and
Marketplace sub-tabs while a customize section is open; the active
sub-tab carries the full selected background while the parent keeps a
subtler one so the two simultaneous highlights read differently.

Also fixes the hover-card flash on click (logo card and session-row
cards): Radix HoverCardContent sits on a DismissableLayer, so a click
on the trigger registers as a pointer-down outside the card and
dismisses it, and the trigger's focus event immediately reopens it.
onPointerDownOutside preventDefault suppresses the dismissal; cards
still close on pointer leave.

* feat(desktop): schedule page row, dialog, and details UX polish

Schedule cards are now click targets: clicking anywhere on a card
outside its controls opens the details dialog (guarded via
closest("button,...") since every inline control, including the Radix
switch, renders a button element), with Enter/Space keyboard support.
The redundant eye button is gone. The remaining edit / run / pause /
delete buttons grow from the 12px icon-sm size to 28px targets with
16px icons, sized consistently with the adjacent enable toggle — the
icons use explicit size-4 classes so the Button base svg rule cannot
shrink them back.

The new/edit dialog gains breathing room between field labels and their
inputs (space-y-2 per field wrapper).

The details dialog no longer scrolls as a whole when the schedule JSON
is long: the dialog is a flex column capped at 85vh, the JSON pre
shrinks to the remaining space (min-h-0) and scrolls internally, and
the Runs tab list scrolls inside the tab the same way.

* feat(desktop): scheduled sessions UX — unified details dialog, run-now handoff, hidden steering, stuck-thinking fix (#13573)

* feat(desktop): merge schedule details into one view and open run-now sessions

The schedule details dialog drops its Overview/Runs tabs: one scrollable
column with the meta grid, the configuration JSON (capped at max-h-64
with internal scroll so it cannot crowd out what follows), and a Runs
section beneath it showing the three most recent runs with a ghost
"Show all N runs" expander (collapsed again whenever a different
schedule's details open). The "Full configuration for this schedule"
subtext is gone; the dialog passes aria-describedby={undefined} so
Radix does not warn about the missing description.

Run now hands you into the session it starts. The trigger command
queues the run and returns before the runner attaches a session id, so
after the toast the handler polls the schedule overview once a second
for up to 15 seconds — which doubles as keeping the page's run status
fresh (refreshSchedules now returns the fetched overview to make that
single-stream) — until the triggered execution reports its session id,
then calls onOpenSession. Guarded so it never auto-navigates after the
user left the page.

* feat(desktop): hide runtime steering messages from transcripts

Scheduled/automation runs inject user-role steering messages each
iteration ("[SYSTEM] This run is not complete until you call
submit_and_exit...", plus a team-obligations variant). The chat view
rendered them as user bubbles, as if the person had typed them — in a
scheduled session the transcript was mostly [SYSTEM] noise.

They are machinery talking to the model, not something the person said
or needs to read, so the transcript now hides them entirely:
MessageBubble renders null for any [SYSTEM]-prefixed user message.
Grouping still treats them as working-row machinery via a single
isSystemSteeringMessage predicate — they collapse into the run's work
span, are never a turn boundary, can never be mistaken for a run's
answer, and never advance the run count even when metadata is missing —
so work-block folding and checkpoint/edit run numbering stay correct.
A finished scheduled session now reads as prompt, work summary, answer.

* fix(desktop): poll history while an attached session's event stream is dead

Opening a scheduled session while (or right after) it runs left the
view stuck on the thinking shimmer until the user switched away and
back. Root cause is in core: the hub daemon executes scheduled runs on
a private LocalRuntimeHost inside createLocalHubScheduleRuntimeHandlers,
while the hub server only projects live events from its own session
host — so session.attach succeeds but no assistant/tool/status events
ever flow. And since multiple hub daemons share cron.db, a run claimed
by a different daemon is invisible to this hub regardless. The proper
core rewiring is tracked as ENG-2474.

Client-side heal that covers every case: while an attached history
session reports a busy status and no chat_event chunk has arrived for
five seconds (and no assistant bubble is mid-stream), poll every three
seconds — re-read canonical history, merged through the same dedupe
path hydration uses, and the session record's status — so the
transcript and the thinking indicator settle in place. Locally driven
turns keep chunks flowing, so the quiet-window guard keeps the fallback
inert there.

* chore(desktop): format workspace selector components

Biome formatting drift that landed on main; picked up by a formatter
pass over components/views/chat.

* fix(desktop): keep stale-stream poll inert during locally driven turns

The fallback poll could fire between a local submit and the model's
first chunk (optimistic user bubble added, stream quiet past the
window, no assistant bubble yet). It then replaced the optimistic
bubble — raw prompt text — with its canonical history twin, which is
stored wrapped in a user_input envelope. The rekey handler that runs
when the stream starts looks for a trailing user bubble matching the
raw prompt, finds only the wrapped copy, and appends a second bubble:
duplicated messages in normal interactive chat.

The poll now stays inert while a local turn is in flight
(turnEpoch !== turnSettledEpoch, or outstanding optimistic user
messages), checked both before polling and again after the snapshot
returns. Hydration marks the turn settled — the mount defaults
(epoch 0, settled -1) otherwise read as an open turn and would keep
the fallback inert forever for the scheduled-session case it exists
for. Applying a polled snapshot also rebuilds the live tool routing
keys, same as hydration, so later tool events update canonical rows
in place instead of appending.

* fix(desktop): keep the working indicator alive for narrating scheduled runs

Watching a scheduled run live: the first tool row appeared, then the
thinking indicator vanished with nothing streaming, and the rest of
the run (final answer, submit_and_exit) only showed up seconds later
in one lump.

inferHydratedChatStatus treats a "running" session record whose
transcript ends on an assistant message as a session that died without
a status flip and reports "completed". That heuristic is right for
stale records, but scheduled/automation models narrate between tool
calls, so a polled snapshot can genuinely end on assistant text
mid-run — the completed flip hid the working indicator, folded the
run early, and disarmed the stale-stream poll (status left the busy
set), dead-ending live updates until an in-flight poll happened to
deliver the finished run.

The heuristic now only applies once the transcript has actually gone
quiet (newest message older than two minutes — comfortably past model
latency plus tool runs). A recently active transcript keeps the
record's "running" verdict, so the indicator stays up and polling
stays armed until the record itself settles.

* fix(desktop): stale-stream poll mirrors the session record instead of inferring

Replaces the previous fix for the vanishing working indicator (the
time-window guard added to inferHydratedChatStatus) with a version
that adds no inference at all: the heuristic is restored to exactly
its long-standing form, and the poll now maps the session record's
status verbatim (mapSessionRecordStatus).

The record is the right authority in the poll's context: the sessions
this fallback serves have a live host maintaining their record, and it
flips to a terminal status when the run ends. Transcript-shape
inference belongs only where it has always lived — hydrating sessions
whose records may be orphaned — and would misread a mid-run snapshot
ending on assistant narration as a finished session, hiding the
working indicator and disarming the poll.

* fix(desktop): address review findings on steering detection and run-now matching

Steering detection additionally requires the injected-message marker
(meta.userRunSpan === 0) beside the [SYSTEM] prefix, so a person's
genuine prompt that happens to start with "[SYSTEM]" stays visible
and turn-counted. The failure direction is deliberate: an unstamped
injected reminder would merely show as a user bubble, while the
content-only check could hide a real prompt.

Run-now only follows the execution id the trigger reply itself named;
the newest-execution-for-this-schedule fallback could open a previous
run's session when the trigger failed to enqueue one.

* fix(desktop): report a failed run-now instead of confirming a start

A trigger reply without an execution means no run was enqueued (the
schedule may have been disabled or deleted since the page loaded). The
handler previously toasted "Run started" regardless and then silently
skipped the session-open polling. It now shows a destructive
"Run not started" toast, refreshes the schedule list so the row
reflects reality, and skips the polling entirely.

* fix(desktop): don't block the main thread on quit while stopping the sidecar (#13566)

Quitting the mac app beach-balled for ~5-7s. The shutdown POST was
built from the ws transport URL (appending /shutdown lands inside the
query string), so the sidecar was never told to exit, and stop() then
polled the child for up to 7s on the main thread - on macOS inside
applicationWillTerminate - before SIGKILLing it.

stop() now sends SIGTERM and returns immediately. The sidecar handles
SIGTERM with the same bounded (5s) graceful shutdown as the /shutdown
endpoint and exits itself, finishing session persistence as an orphan.

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

* feat(desktop): hover trash button on sidebar session rows (#13582)

Each session row shows a trash icon on the right while hovered (or
when the button itself is focused), opening the same delete
confirmation dialog the row's context menu uses. The row is a button
and buttons cannot nest, so the trash is an absolutely positioned
sibling inside a group/row wrapper, overlaid where the timestamp sits:
row hover hides the timestamp, shows the trash, and moves the row's
hover background to the wrapper group so it holds while the pointer is
on the trash itself.

* fix(desktop): install marketplace plugins and MCP servers in-process instead of spawning a cline binary (#13585)

* fix: install marketplace plugins and MCP servers in-process instead of spawning a cline binary

The desktop app sidecar and cline-hub shelled out to 'cline plugin install'
and 'cline mcp install' for marketplace installs. Packaged GUI apps inherit
launchd's minimal PATH on macOS and most desktop users have no cline CLI
installed at all, so installs failed with a red
'Executable not found in $PATH: "cline"' error.

Install via @cline/core's installPlugin/installMcpServer in-process instead,
matching what the VS Code extension already does. Also fix
parseMcpInstallArgs in @cline/core to treat the marketplace catalog's '--'
separator as end-of-options; previously the separator itself became the
stdio command.

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

* refactor: drop test-injection plumbing from marketplace installers

Call @cline/core's installPlugin directly instead of threading an
installer option through the marketplace entry points; tests stub the
core module instead.

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

* revert: keep cline-hub marketplace installs CLI-backed

The hub dashboard is launched via 'cline dashboard', so a CLI is always
present and CLINE_WRAPPER_PATH resolves it; the PATH bug only affects
the desktop app, which does not ship a CLI.

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

---------

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

* chore(desktop): release v0.0.18

* chore(vscode): release v4.1.16

* chore(sdk): release v0.0.80

* chore(cli): release v3.0.59

* fix(hub): stop shipping full transcripts inside broadcast hub events (#13587)

* fix(hub): stop shipping full transcripts inside broadcast hub events

Every session.updated (and session.created/detached/run.started) event
embedded the session's ENTIRE message transcript via readCoreSessionSnapshot,
even though no consumer reads snapshot.messages off an event — clients fetch
messages with the session.messages command. For a multi-megabyte transcript
this turns every status flip into megabytes per subscriber, floods the
durable event log, and (until the send-queue backpressure fix lands) lets a
slow subscriber balloon the hub process by one full transcript copy per
event — reported as a 25GB cline process on a 16GB Mac.

Strip snapshot.messages centrally in HubServerTransport.publish() so every
current and future event publisher is covered, the event log stores slim
envelopes, and cursor replay stays byte-identical with live fan-out. All
other snapshot fields (status, usage, model, workspace, checkpoint) are kept,
and command replies are untouched.

* fix(hub): never capture the transcript into event/reply snapshots

Replaces the publish-boundary strip with the real fix: don't build
message-bearing snapshots in the first place. emitSessionSnapshot no longer
re-reads the entire transcript from disk on every status flip, and
readCoreSessionSnapshot no longer reads it for any event or reply — a
snapshot is a state notification (status, usage, model, workspace,
checkpoint); the transcript is fetched via the session.messages command.
Checkpoint-restore snapshots (session-versioning-service) are untouched:
restore replies carry messages in their own dedicated field.

* chore(desktop): release v0.0.19

* chore(sdk): release v0.0.81

* chore(cli): release v3.0.60

* fix(vscode): avoid render crash on malformed api_req payloads in combineApiRequests (#13560)

* fix(vscode): stop pinning DeepSeek model count in catalog smoke test (#13600)

* feat(ui): share agent welcome hero (#13567)

* feat(ui): share agent welcome hero

* test(ui): cover welcome hero pointer states

* refactor(ui): keep welcome hero API minimal

* test(ui): verify welcome hero package assets

* fix(ui): inline welcome hero masks

* fix(tools): preserve a file's own CRLF line endings across apply_patch updates (#13512)

* fix(desktop): keep the window title bar draggable across views (#13572)

* fix(desktop): keep window title bar persistent

* fix(desktop): reserve persistent title bar space

* fix(desktop): polish persistent title bar layout

* Sign Windows CLI binaries with Azure Trusted Signing; surface app-control launch errors (#13021)

* feat(cli): sign Windows binaries with Azure Trusted Signing and surface app-control launch errors

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

* fix(cli): use _CLI-suffixed signing profile secret, normalize endpoint, fail loud on partial config

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

---------

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

* Tunnel ProtoBus over the existing Host Bridge (#13218)

* feat(core): tunnel ProtoBus over Host Bridge

* fix(core): harden Host Bridge stream lifecycle

* fix(core): serialize concurrent chunked responses per request

Streaming handlers deliver updates fire-and-forget, so two logical
responses for one request_id can be in flight at once. Chunked payloads
made forwarding non-atomic: each chunk write is an await, so concurrent
forwards could interleave their chunk sequences and the receiver --
which reassembles purely by arrival order -- would splice two payloads
into one. Route all forwards for a request through one promise chain; a
failed write rejects every later forward so a torn payload is never
followed by more chunks.

Rename the lock manager's instanceAddress to instanceOwner: it holds an
opaque per-spawn instance ID on the token path and a listener address
only on the CLI-harness path. Delete the caller-less getInstanceByPort
query that interpreted the owner as an address.

Also: document message_json as a legal wire encoding for small
payloads, close the gRPC client when startup fails, note the
intentional discard of the cancellation confirmation, and add the
proto's trailing newline.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* Build and Authenticode-sign a Windows x64 desktop installer in desktop releases (#13607)

* feat(desktop): build and Authenticode-sign a Windows x64 NSIS installer in desktop releases

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

* fix(desktop): pin OIDC-adjacent actions to commit SHAs in the Windows signing job

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

* fix(desktop): pin checkout and upload-artifact to commit SHAs in the Windows signing job

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

---------

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

* fix(desktop): show agent-created schedules on the Schedules page (#13613)

* fix(desktop): show agent-created schedules on the Schedules page

Schedule hub commands are scoped to the workspace registered by the
connection, but the desktop app's hub client registers the app launch
directory while agent-created schedules live under each chat's own
workspace folder - so they never appeared on the Schedules page.

Grant token-authenticated hub connections (which can already bind any
workspace at registration) explicit cross-workspace schedule access via
an allWorkspaces payload flag, and have the desktop sidecar request it
for routine schedule commands. Workspace-bound clients (local browser
origins) and default CLI behavior stay scoped.

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

* refactor(core): strip allWorkspaces flag from schedule inputs and pin it in the sidecar payload

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

---------

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

* Make suggested routine template prompts prescriptive about their final output (#13611)

* Make bug hunter routine template prescriptive about its final report

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

* Make remaining routine templates prescriptive about their final output

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

---------

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

* Desktop: surface scheduled-task final output — auto-expand submit_and_exit and render its summary as markdown (#13612)

* desktop: auto-expand submit_and_exit and render its summary as markdown

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

* desktop: render submit summary in full foreground color

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

* desktop: label the submit row 'Scheduled task completed'

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

* desktop: label errored submit_and_exit rows as failed

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

---------

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

* Add tooltips explaining Live and After recording badges on voice input models (#13610)

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

* Remove box shadow from chat message actions row (#13630)

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

* fix(desktop): make the Tauri shell work on Windows (#13632)

- Defer updater installation to the user-initiated restart on Windows:
  install() launches the NSIS installer and exits the process immediately,
  so the background cycle now downloads only and stages the bytes, and
  restart_to_apply_update installs them after stopping the sidecar.
- Spawn child processes (sidecar, git, cmd /C start) with CREATE_NO_WINDOW
  so the GUI-subsystem app doesn't pop visible console windows.
- Fall back to USERPROFILE when HOME is unset resolving the MCP settings
  path, matching the sidecar's homedir().
- Reap the sidecar after the Windows hard-kill so its exe file lock is
  released before the NSIS installer replaces it.

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

* fix(core): stop watching agenda spec dirs while the todo tool is disabled (#13629)

* fix(core): stop watching agenda spec dirs while the todo tool is disabled

Since #13530 disabled the agent todo tool, the Agenda UI, and the
automation pump, the hub still created fs.watch watchers on the global
agenda specs dir and on every workspace root recorded in the task store
(at startup and on scope access). Nothing consumes the watcher-driven
task events while the feature is off, and the task.* hub commands
already reconcile spec files on demand, so the watchers are pure
overhead - one OS watch handle per known workspace.

Wire watchFiles to AGENDA_TODO_TOOL_ENABLED the same way
automationEnabled is, preserving a host's explicit watchFiles opt-out
for when the flag is turned back on. Schedules are unaffected: the
schedule list has no file watcher and updates through hub commands and
published schedule events.

* fix(core): reconcile external spec edits inside updateTask

With the spec watchers off there is no background reconciliation, so a
task spec edited directly on disk made every same-store task.update fail
the signature check with "task spec changed outside the manager" until
an unrelated task.get or task.list happened to reconcile the scope.

Reconcile the task's scope at the start of updateTask (mirroring what
refreshAndVerifyTaskIntent already does for approve/run), skipping it
when the file reconciler itself is the caller to avoid recursing from
reconcileFileStore. An external edit now surfaces as the store's normal
stale-revision conflict, and a re-read-and-retry succeeds. This also
closes the pre-existing watcher debounce race for updates.

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently (#13565)

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently

Port the cline-provider refresh semantics to openai-codex and oca:
a transient refresh failure (network error, timeout, server 5xx) with an
already-expired access token now rethrows instead of returning null.
A null return means the refresh token was REJECTED and re-auth is
required; treating an outage blip as a rejection is what turned it into
a forced 'openai-codex requires re-authentication.' task stop while the
settings UI still showed the user as signed in.

Both providers also emit user.auth_refresh_soft_failure telemetry on
transient failures (the 'prevented logout' counter the cline provider
already has) and attach status/errorCode details to the genuine
invalid_grant logout event.

* refactor: collapse duplicate soft-failure telemetry branches and test

Review feedback: compute tokenExpired once and emit the soft-failure
event once in both providers, then return current credentials or
rethrow. Fold the codex soft-failure telemetry assertions into the
existing still-usable-token test instead of a near-duplicate case.

* fix: make OpenAI Codex (ChatGPT subscription) sign-in fail loudly instead of silently dead-ending (#13537)

* fix: make OpenAI Codex sign-in fail loudly instead of silently dead-ending

When callback port 1455 is already in use (e.g. by the Codex CLI or a
previous pending sign-in), startLocalOAuthServer returns a no-op server
and loginOpenAICodex would open the browser anyway, then dead-end:
the callback could never be received, and in the VS Code extension the
user just saw nothing happen after clicking 'Sign in to OpenAI Codex'.

- loginOpenAICodex now fails fast with an actionable 'port in use'
  error before opening the browser, unless the host provides manual
  code entry (the CLI's paste fallback keeps working)
- surface OAuth redirect errors (e.g. access_denied) instead of
  collapsing them into 'Missing authorization code'
- the extension dedupes concurrent sign-in clicks: a re-click re-opens
  the auth page of the pending flow instead of spawning a second flow
  that would collide with our own callback server
- browser-open failures now show an error message with the URL to
  open manually instead of only logging
- abandoned-flow timeouts no longer surface a confusing 'Missing
  authorization code' toast

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

* refactor: drop host-side codex login dedupe, keep flow identical to CLI

The SDK owns the failure handling now (fail-fast on an unbindable
callback port), so the extension keeps the exact same simple
loginOpenAICodex call the CLI uses. A second click while a flow is
pending gets the SDK's clear port-in-use error, same as running
'cline auth openai-codex' twice would. Keep only the CLI-parallel
onOpenUrlError surfacing (the CLI prints 'open the URL above
manually'; the extension's equivalent is an error toast with the
URL).

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

* test(e2e): cover Codex sign-in callback-port failure and redirect errors

Two driven-VS Code tests for the OpenAI Codex (ChatGPT subscription)
sign-in flow:

- with port 1455 occupied on both loopback families, clicking the
  sign-in button surfaces the fail-fast port-in-use toast
- with the port free, the callback server binds and an OAuth redirect
  error (access_denied) propagates to a visible error toast

The second test opens a real browser tab to the OpenAI auth page as a
side effect of the genuine sign-in click.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* feat(core): anchor agent-created schedules in the user's .cline schedules home (#13634)

* feat(core): anchor agent-created schedules in the user's .cline schedules home

Agent-created schedules inherited whichever workspace folder the chat
session happened to run in, scattering user-level routines across chat
and project folders. They were invisible to workspace-scoped listings
elsewhere, tied to folders that may be cleaned up, and each chat's
tasks tool saw a different set when checking for duplicates.

Anchor them in ~/.cline/schedules instead: the hub's scheduled-task
session defaults now resolve to that home (created on demand), so
agent-created schedules live and run in one stable user-level scope.
The tasks tool guidance now tells agents that scheduled sessions run in
the schedules home, so prompts must carry absolute paths to any project
they operate on.

Schedules created explicitly with a workspace (CLI --workspace, desktop
routine wizard) are unchanged, and existing rows keep their current
workspaceRoot - they stay visible through the all-workspaces listing
paths (#13613, #13633).

* test(core): restore any pre-existing CLINE_DIR after the agenda hub test

The test's cleanup deleted CLINE_DIR outright, so an environment that
had it configured would leave later tests in the same worker on the
default storage directory. Save the previous value and restore it.

* test(core): restore CLINE_DIR even when hub test setup throws early

Restoring the override in the try/finally missed failures thrown during
transport construction or start(), before the try was entered. Register
the restore with onTestFinished instead, which runs regardless of where
the test fails.

* fix(desktop): don't show providers as configured without real credentials (#13608)

* fix(desktop): don't show providers as configured without real credentials

The desktop settings marked any provider with a persisted settings entry
as Configured, but legacy VS Code migration and empty saves can seed
entries (e.g. qwen-code, sapaicore) holding only a default model and no
credentials. Move the CLI's isProviderSettingsUsable readiness check into
@cline/core, expose it as a computed 'configured' flag on the provider
catalog, and use it in the desktop's isProviderConnected.

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

* fix(desktop): resync catalog after saves so Configured badge updates live

Optimistic provider mutations can't know the sidecar-computed 'configured'
flag, so after connecting a keyless provider or saving cloud credentials
(e.g. a Vertex project id) the row stayed 'Not configured' until remount.
Silently refetch the catalog after each successful save, guarded by the
existing generation counter so newer edits discard stale responses.

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

* fix(desktop): claim a generation in post-save resync so overlapping refreshes can't apply stale snapshots

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

* fix(desktop): bump catalog generation on OAuth login success

Every other optimistic provider mutation claims a new generation; the
OAuth success path didn't, so a catalog load or resync still in flight
could arrive late and overwrite the just-connected state.

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

* fix(desktop): resync catalog after OAuth login instead of bare generation bump

The resync claims a new generation (discarding any stale in-flight
response) and its own fetch covers both the new OAuth connection and any
provider saved moments earlier, matching the post-save path.

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

---------

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

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint (#13626)

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint

Restoring a checkpoint runs git reset --hard, which moves the current
branch pointer. If commits were made after the checkpoint (by the user
or by the agent), the reset silently knocked them off the branch,
leaving them reachable only through the reflog.

Guard the reset: if HEAD no longer matches the commit the checkpoint
was created on, throw a descriptive error (including how many commits
would be dropped) instead of destroying history. Chat-only restore is
unaffected, and users who really want to discard the commits can reset
the branch manually first.

Fixes #13550

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

* fix(core): close the guard-to-reset race with an atomic ref update

The moved-HEAD guard read HEAD, ran further git commands, then reset
unconditionally, so a commit landing in that window could still be
knocked off the branch. Replace the reset's branch move with git's
native compare-and-swap (git update-ref HEAD <new> <old>), which fails
if HEAD no longer points at the verified commit, and follow with a bare
reset --hard to sync the index and worktree to the already-moved HEAD.

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

---------

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

* fix: hide history cost estimates for subscription-billed tasks (#13562)

* fix(vscode): hide history cost estimates for subscription-billed tasks

The task-header fix for subscription providers cannot reach history:
history rows render the stored totalCost (an API-rate estimate) and do
not know which provider ran the task, so the history page printed
$X.XXXX on every row and the recent-task chips in an empty chat view
rendered a $ chip even for subscription-billed tasks.

The SDK session records already persist the provider — the CLI's
history view uses it for exactly this — but the VS Code mappers dropped
it. Map it through both transports (HistoryItem.apiProvider for the
state-pushed taskHistory, TaskItem.api_provider for getTaskHistory) and
suppress the dollar figure per row when that provider's
usageCostDisplay is not "show", via a new useUsageCostVisibility
predicate shared by both surfaces.

Rows without a recorded provider (tasks predating the field, legacy
imports) keep showing the stored value — there is nothing to key
suppression on.

* test(vscode): e2e-verify history cost suppression in real VS Code

Seeds SDK session records (one openai-codex subscription task, one
anthropic usage-billed task) into the isolated CLINE_DIR before the
webview loads, then asserts in a real VS Code instance that both the
recent-task chips and the full history page render the dollar figure
only for the usage-billed task. Covers the two boundaries the unit
tests stub: on-disk records reaching getTaskHistory with provider
populated, and the provider listings delivering the subscription mark
to the webview.

* Fix scheduled tasks disappearing after desktop app updates (#13627)

* Fix hub-managed schedules being wiped by cron reconciliation on hub restart

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

* Require the virtual hub/schedules path when exempting specs from removal reconciliation

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

* Treat recorded source mtime as proof a spec is file-backed, closing the hub/schedules spoof gap

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

---------

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

* fix(shared): discover global rules at ~/Cline/Rules (#13614)

The VS Code Rules tab resolves the Documents folder via
'xdg-user-dir DOCUMENTS', which prints bare $HOME when no user-dirs
config exists (WSL/headless installs), so it reads and writes global
rules at ~/Cline/Rules. The SDK's rule search paths only covered
~/Documents/Cline/Rules, so those rules never reached the system prompt.
Add the missing path to the search list.

Fixes #13542

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

* feat: add searchable session history (#13420)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

---------

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

* Fix CLI crash when a remote MCP server is offline but enabled (#13639)

Remote (SSE/streamable HTTP) MCP connects run on the session.create
critical path, which the hub caps at 30s. Without a connect budget an
unreachable server spent the full 60s default request timeout (with the
SSE transport stuck in a reconnect loop), stalling session.create past
the hub deadline and tearing the whole session down - the interactive
TUI exited and one-shot runs failed. Stdio servers already have a
bounded initialize budget for exactly this reason; give URL clients the
same treatment with a 10s default connect budget that an explicit
timeout overrides in either direction.

Fixes #13597

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

* test(vscode): prevent E2E worker teardown hangs (#13644)

* test(vscode): capture external URLs in E2E runs

* docs(test): clarify browser capture rationale

* Add a GitHub integration step to the onboarding (#13225)

* Add feature flags to the app

* React to account updates

* Address comments

* Add a GitHub integration step to the onboarding

* validate domain and fix errors on auth

* Hide the step behind a feature flag

* update version

---------

Co-authored-by: John Choi <john.choi@cline.bot>

* fix(ci): stop e2e worker teardown timeouts and deflake hub daemon e2e on Windows (#13646)

* fix(e2e): stop VS Code e2e worker teardown from timing out

The ext-vscode-test-e2e job has been failing on main with 'Worker teardown
timeout of 60000ms exceeded' even though every test passes. Playwright only
reports an Electron app as closed once the process exits AND every holder of
its stdio pipes is gone (ChildProcess 'close' waits on the extra fd3/fd4
pipes Playwright creates for Electron). Any VS Code descendant that outlives
the main process (chrome_crashpad_handler, GLib's 'dconf watch' helper,
xdg-open browser handlers, VS Code 1.135's agent host CLI subprocess that
logs 'unable to kill the process') keeps those pipes open, so app.close()
never resolves and the worker teardown hangs on it until its 60s timeout
fails the job.

Harness fixes, each removing one source of that wedge:

- closeAppForTeardown now SIGKILLs the whole process group (taskkill /T on
  Windows) when app.close() times out, instead of only the main pid — and
  does so even when the main process already exited, which is exactly the
  wedged state. Playwright launches Electron detached, so pid == pgid.
- Launch VS Code with --disable-crash-reporter so no crashpad handler
  outlives the app holding the harness pipes.
- Seed the fresh user-data-dir with chat.disableAIFeatures: true so VS
  Code's own AI features (rolled out via server-side experiments, so CI
  breaks without any repo change) never start their agent host process.
- Drop the page.close() teardown: closing VS Code's last window quits the
  whole app, and ElectronApplication.close() on an already-exited app
  deadlocks; the app fixture's app.close() closes windows itself while the
  app is alive.
- Codex sign-in no longer opens a real external browser under E2E_TEST; the
  codex-oauth test drives the OAuth callback itself, and the browser was an
  orphaned process holding the harness pipes on the runner.

* fix(core): deflake hub daemon e2e tests on Windows runners

sdk-test on windows-latest fails intermittently in the hub daemon e2e
files:

- shutdown.e2e.test.ts dies with a bare 'Error: socket hang up'. That
  message is the ws handshake (http.ClientRequest) failing, not the
  /shutdown fetch (an undici failure prints 'TypeError: fetch failed'):
  a freshly spawned bun daemon on a loaded 2-core Windows runner
  occasionally drops its first accepted connection before writing the
  upgrade response. Real hub clients reconnect with backoff, and the test
  asserts shutdown behavior rather than first-connection reliability, so
  openAuthenticatedSocket now retries transient handshake failures within
  a 15s budget.
- singleton.e2e.test.ts times out waiting for daemon discovery: it still
  used the 10s hang guard that 0cfc90158 already raised to 30s in
  shutdown.e2e.test.ts for the same reason. Use the same 30s guard.
- Raise the e2e testTimeout to 60s so a test that legitimately spawns two
  daemons back to back can survive slow-runner startups instead of the
  discovery hang guard being cut off by the test timeout.

* feat(desktop): render tool output images as attachments (#13643)

* fix(desktop): render tool output images as attachments

Add support for displaying media returned by tool calls (e.g. screenshots)
as rendered images with expand-to-fullscreen capability instead of raw
base64 text. Introduces an `ImageCarousel` component for navigating
multiple images, propagates the expand handler to tool message blocks,
and extracts/validates output media in tool summaries.

* test: cover multi-image and canonical media extraction in tool output (#13645)

extractOutputMedia and the desktop tool-message rendering path were only
ever exercised with exactly one distinct valid image, and
canonicalInlineMedia (MCP-style type: "media" blocks for audio/video/file)
had zero coverage. Add tests for: multiple distinct images in one tool
result (parser + desktop carousel navigation), inline audio via the
mime_type key spelling, canonical video/file media blocks, and rejection
of an invalid canonical image block.

---------

Co-authored-by: Harrison <harrison@cline.bot>

* chore(desktop): release v0.0.20

* feat(sdk): add discovery boundary ahead of Agent Plugins support (#13017)

* ENG-2490: Propagate session aborts to teammates (#13647)

* fix(core): propagate session abort to teammates

* fix(core): persist aborted teammate tasks as cancelled

* fix(core): settle teammate work on session abort

* fix(core): isolate replacement runs from stale aborts

* refactor(core): narrow teammate task status metadata

---------

Co-authored-by: abeatrix <beatrix@cline.bot>

* fix(llms): use AI SDK 7 Langfuse telemetry (#13651)

* fix(llms): use AI SDK 7 Langfuse telemetry

* test(llms): cover Langfuse runtime context

* chore(llms): built-in model list update 1787907289186 (#13663)

* chore(llms): built-in model list update 1787907289186

Result of `bun run build:models`.
Includes updated model list and fixed formatting issues across codebase.

* test(llms): update GLM reasoning toggle expectation

* test: cover session search fallback on hub timeout and rejection (#13642)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

* test: cover sidecar search fallback on hub timeout and rejection

The existing search_sessions tests only exercised the index-hit and
empty-index-fallback paths with an immediately-resolved hub reply.
Add coverage for the two other realistic Hub-connection failure
modes the fallback is meant to tolerate: the hub call rejecting, and
the hub call hanging past the 750ms withSearchDeadline race.

---------

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

* fix(core): refresh Cline models from live catalog (#13670)

* feat(ui): share attachment drop zone (#13672)

* feat(ui): share attachment drop zone

* fix(ui): cancel disabled attachment drops

* chore(ui): simplify drop zone surface

* chore(ui): release v0.2.0-next.8

* Chore/bump undici mermaid (#13675)

* chore(deps): bump mermaid to 11.16.1 and raise undici floor to 7.29.0

* chore(deps): patch js-yaml and body-parser in the npm-managed subprojects

* fix(llms): make Langfuse tracer detection survive minified release builds (#13680)

* fix(llms): recognize direct tracer providers

* fix(llms): make Langfuse tracer detection survive minified release builds

Release binaries are compiled with minify enabled, which renames classes,
so initializeLangfuseTelemetry's constructor-name guard never matched
"ProxyTracerProvider" and silently returned readiness=false in every
production build (hub log: "creating span processor" followed by
"initialized readiness=false" with no branch message in between). Dev runs
execute unminified source, which is why the same env vars worked there.

Replace every constructor-name comparison with checks that survive
minification: detect the proxy structurally via getDelegate, distinguish a
recording provider from the no-op fallback by its lifecycle methods, and
confirm our NodeTracerProvider registration by object identity. When a
foreign provider already owns the global slot, attach the Langfuse span
processor to it when it accepts processors, and otherwise shut down the
orphaned provider and report the rejection instead of bailing silently.

Verified by bundling the module with Bun minify:true against the real
OpenTelemetry packages: the previous code reproduces readiness=false
(provider class name mangles to "H2"), the new code initializes with
readiness=true.

* fix(vscode): prevent hook spawn failures from crashing the core process (#13422)

* fix(vscode): prevent hook spawn failures from crashing the core process

A hook child-process spawn failure emitted "error" on HookProcess with no
listener registered, which Node's EventEmitter turns into an uncaught
exception - killing the entire cline-core process instead of failing the
one hook open. Guard the emit behind listenerCount so the rejection (which
StdioHookRunner handles) is the only propagation path.

The trigger was a workspace root that no longer exists on disk passed as
the spawn cwd: Node reports a nonexistent cwd as a misleading ENOENT on
the launcher binary ("spawn /bin/sh ENOENT"). Validate cwd existence in
HookProcess right before spawning - falling back to no explicit cwd with
a warning that names the missing directory - and when a spawn still fails
ENOENT because the directory vanished in between, name it in the error
message instead of blaming the shell.

* fix(vscode): fail hooks with a missing working directory instead of relocating them

Running a hook whose assigned cwd no longer exists from the host
process's own working directory would let its relative paths read and
write an unrelated location (e.g. the IDE install directory). Reject
before spawning, with an error naming the missing directory; the runner
reports the hook as failed and the task continues. Also carry pre-spawn
failure messages into HookExecutionError details so the cause is not
reduced to a bare "exited with code 1".

* fix(vscode): thread task id into hook runner creation so execution telemetry fires (#13547)

The SDK hooks adapter created every hook runner without a task id, and
StdioHookRunner gates all captureHookExecution calls on one being set —
so the next variant emitted zero hooks.execution events while discovery
telemetry fired normally. Pass the task id (and tool name for the tool
hooks) at all five factory.create call sites, and pin the threading
with a regression test.

* Desktop marketplace redesign: two-pane explorer with full catalog metadata (#13653)

* feat(desktop): add marketplace design exploration prototypes (storefront, explorer, registry)

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

* fix(desktop): render catalog icon tiles without percentage padding

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

* feat(desktop): drop placeholder icon tiles from explorer marketplace direction

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

* feat(desktop): make explorer the marketplace view, drop design exploration harness

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

* feat(desktop): add category tag filters to marketplace explorer

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

* feat(desktop): collapse marketplace category pills behind a more toggle

* feat(desktop): remove maturity badges and CLI install section from marketplace

---------

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

* fix(core): propagate parent aborts to delegated subagents (#13677)

* fix(core): propagate parent aborts to delegated subagents

* docs(core): narrow delegated abort guarantees

* fix(core): release delegated sessions after execution

* fix(core): scope abort listeners to active runs

* fix(core): inherit parent runtime pid for subagents

* fix(desktop): keep Stop available for running child agents (#13678)

* fix(desktop): keep Stop available for running child agents

* fix(desktop): reconcile aborted tool activity

* fix(desktop): guard abort and agent polling races

* fix(desktop): preserve authoritative abort status

* fix(desktop): track queue-verified completion

* test(desktop): trim duplicate abort coverage

* fix(desktop): settle delayed queue verification

* fix: sanitize stored API keys and make provider credential rejections actionable (#13549)

* fix(vscode): sanitize pasted provider API keys at the settings write boundary

Clipboards smuggle control and invisible formatting characters (newlines,
zero-width spaces, BOM) into pasted API keys. The masked key field hides
the corruption and providers reject the key with a 401 indistinguishable
from a genuinely wrong key. Strip those characters and surrounding
whitespace once in the provider config store write path, so both backing
stores (legacy state secrets and providers.json) receive the clean value.
A whitespace-only value now clears the key.

* feat(llms,vscode): classify provider 401/403 as auth errors and surface actionable guidance

Add an "auth" ProviderErrorClass, assigned when the HTTP layer reports
401/403 — status-only on purpose, since provider bodies can quote words
like "unauthorized" without the request being an auth failure. The class
rides the existing errorClass plumbing (finish -> run-failed ->
AgentErrorEvent), so every host receives it with no new wiring.

In the VS Code chat surface, rewrite classified credential rejections
from BYOK providers into actionable text pointing at the API key
configuration, keeping the provider's raw body as a diagnostic tail.
Raw bodies alone are dead ends: Mistral, for example, answers an
identical {"detail":"Invalid API Key"} for a wrong, empty, or
wrong-scope key. Cline-account providers keep the JSON path so the
webview still renders their auth failures as a sign-in card.

* Fix ask-question option text not wrapping (#13718)

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

* chore(desktop): release v0.0.21

* fix(core): stop an empty capability list from stripping image input (#13583)

`modelHasCapability` documents a missing or empty capability list as
carrying no signal, so each gate declares its own default. Two readers
bypassed it and read `capabilities` directly, where an empty list is not
nullish but `[].includes(x)` is false:

- the session runtime's `modelSupportsImages` metadata used
  `capabilities?.includes("images") ?? true`, so the intended fail-open
  never fired for an empty list and the file-read tool silently dropped
  every image from the request;
- `toProviderModel` projected an empty list onto `false`, telling pickers
  a model definitively lacks vision, attachments, and reasoning when
  nothing had been declared.

Both now route through the shared helpers, which state their unspecified
default explicitly: `modelSupportsImageInput` fails open for a capability
gate, and `declaredCapability` preserves `undefined` for `ProviderModel`'s
tri-state booleans. A populated list stays authoritative in both.

A thinking config now short-circuits `supportsReasoning` instead of being
OR-ed with the capability read, so its absence no longer collapses the
tri-state to `false`.

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* fix(llms): translate gateway capabilities in one place (#13584)

* fix(core): stop an empty capability list from stripping image input

`modelHasCapability` documents a missing or empty capability list as
carrying no signal, so each gate declares its own default. Two readers
bypassed it and read `capabilities` directly, where an empty list is not
nullish but `[].includes(x)` is false:

- the session runtime's `modelSupportsImages` metadata used
  `capabilities?.includes("images") ?? true`, so the intended fail-open
  never fired for an empty list and the file-read tool silently dropped
  every image from the request;
- `toProviderModel` projected an empty list onto `false`, telling pickers
  a model definitively lacks vision, attachments, and reasoning when
  nothing had been declared.

Both now route through the shared helpers, which state their unspecified
default explicitly: `modelSupportsImageInput` fails open for a capability
gate, and `declaredCapability` preserves `undefined` for `ProviderModel`'s
tri-state booleans. A populated list stays authoritative in both.

A thinking config now short-circuits `supportsReasoning` instead of being
OR-ed with the capability read, so its absence no longer collapses the
tri-state to `false`.

* fix(llms): translate gateway capabilities in one place

Three producers built gateway model definitions from catalog `ModelInfo`,
and each carried its own hand-written `switch` over the capability list.
Nothing tied them together, so they drifted:

- builtin providers always emitted a capability list, so a model whose
  catalog entry declares no capabilities became `["text"]` where the other
  producers emitted `undefined`. `modelSupportsToolCalling` fails open only
  for an absent or empty list, so that list read as an authoritative denial
  and stripped every tool definition from requests to the affected language
  models (dify, sapaicore, opencode, and the Codex CLI);
- the OpenAI-compatible path mapped an `audio` capability that
  `ModelCapabilitySchema` does not define, while the other two dropped it;
- the pass-through capabilities (`streaming`, `files`, `temperature`, ...)
  were enumerated explicitly in one, folded into `default:` in another,
  and ignored in the third.

One exported `toGatewayModelCapabilities` now serves every producer. It is
built on a `Record<ModelCapability, GatewayModelCapability | null>` rather
than a `switch`, so extending `ModelCapabilitySchema` without deciding the
new capability's mapping fails to compile instead of silently falling
through to a default.

The conformance tests walk the capability state space taken from
`ModelCapabilitySchema` itself and assert the real producers agree with the
translator, so a future producer that maps capabilities on its own fails
even when the translator's own unit tests still pass.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>

* fix(cli): keep markdown streaming prop stable to stop settle flash (#13719)

Flipping the <markdown> streaming prop from true to false when an
assistant text segment settles makes MarkdownRenderable call
updateBlocks(true), which skips every block-reuse path and destroys and
recreates all block renderables. Until tree-sitter re-highlights them
the whole message renders blank/unhighlighted, which users see as the
text flashing at the end of each response. Keep streaming={true} for
the transcript markdown (opencode's TUI does the same); entry.streaming
still drives the spinner glyph.

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

* Clarify model-facing message when user rejects a tool call (#12673)

* Clarify model-facing message when user rejects a tool call

* Include the rejected tool's name in denial reasons

* Move user-rejected tool reason into @cline/shared

* Route new user-rejection approval paths through shared reason builder

Since the original PR, several new approval surfaces landed on main with
their own terse denial strings (CLI connectors, ACP permissions, Cline Hub
webview, desktop webview, example VS Code extension). Route all of them
through buildUserRejectedToolReason so the model sees a consistent,
non-error rejection message.

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

* Add buildUserRejectedToolReason to the @cline/shared integration-test stub

The VS Code integration tests run the tsc-built CJS tree and stub the
ESM-only @cline/shared package in test-setup.js; the stub was missing the
new export, so tool-approval-denial.js threw at module load in CI.

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

* Trim scope back to the minimal rejection-copy fix

Restore the connector deniedReason plumbing, ACP permission strings,
desktop webview reason, example extension reason, and hub server fallback
to their main versions. Those surfaces already attribute the denial to a
user and are outside ENG-2329. Keep the Cline Hub webview change since
that path emits its own rejection string the model sees.

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

* Move rejection guidance suffix into agent runtime per review

* Apply review suggestions: neutral fallback reason and -- separator before rejection suffix

---------

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

* Default web search on for the desktop app (#13725)

* Default web search on for the desktop app

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

* Make desktop web search default seed best-effort

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

---------

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

* fix(desktop): reconcile experimental sync behavior

* fix(desktop): enable macOS voice input (#13741)

* Promote ClinePass across home banner, account page, and settings (#12556)

* feat(webview): promote ClinePass across home banner, account page, and settings

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

* fix(webview): drop removed ext-cline-pass flag gating and hardcoded pricing from ClinePass promos

The ext-cline-pass feature flag no longer exists (the provider is ungated on
main), so promo surfaces are now gated only on self-hosted mode and org
remote-config provider allowlists. Promo copy describes the subscription
without a hardcoded price, matching the CLI copy cleanup in #13514.

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

* fix(webview): open the personal dashboard context from ClinePass subscription links

ClinePass always bills the personal account, but the Manage Subscription
button (and the ClinePass provider's usage link) landed org-context users
on the org dashboard. Pass personal=true like EntitlementError and the
CLI subscription links already do.

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

---------

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

* Desktop marketplace: show detail panel only on item click, left-align detail content (#13747)

* Desktop marketplace: show detail panel only on click, left-align detail content

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

* Desktop marketplace: drop license cell, single Learn more link (homepage, else repo)

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

* Desktop marketplace: keep selected entry open while list is filtered

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

---------

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

* feat(desktop): import sessions from Claude Code, Codex, and opencode (#13744)

* feat(core): session import service for Claude Code, Codex, and opencode history

Adds a SessionImportService to @cline/core that discovers sessions in the
on-disk stores of Claude Code (~/.claude/projects JSONL), Codex
(~/.codex/sessions rollouts + session_index titles), and opencode
(opencode.db sqlite), translates each conversation into Cline's native
MessageWithMetadata format, and persists it through CoreSessionService as
a completed, listable, resumable session.

Key mechanics:
- Claude Code: parentUuid tree walk from the newest leaf picks the active
  branch (edits/retries branch the log); same-message.id assistant lines
  merge back into one turn; sidechains, meta lines, and slash-command
  wrappers are excluded; ai-title/summary lines provide titles.
- Codex: real prompts come from user_message event_msg lines (user-role
  response_items are injected AGENTS/environment context, with a fallback
  for old rollouts); function_call/output pairs map to tool_use/tool_result;
  resumed rollouts that re-embed the original session id dedupe to the
  richest file; token_count events stamp per-turn metrics.
- opencode: reads a temp snapshot of the WAL-mode db; inline tool parts
  split into tool_use + tool_result to preserve provider-valid structure;
  child (subagent) sessions and synthetic parts are skipped.
- Shared sanitizer guarantees replayability: orphaned tool_use gets a
  placeholder result, orphaned tool_results and empty text blocks drop,
  provider-session-scoped signatures/encrypted reasoning strip.
- Imported sessions pass every history-visibility gate (terminal status,
  non-empty provider/model, chat-workspace fallback cwd, no fabricated
  checkpoint metadata) and carry metadata.importedFrom for idempotent
  re-discovery (alreadyImportedSessionId).

* feat(desktop): sidecar commands for importing sessions from other tools

Adds two sidecar WebSocket commands backed by @cline/core's
SessionImportService:

- list_importable_sessions: returns { installedTools, sessions } where
  sessions are ImportableSessionSummary rows (tool, sourceId, title, cwd,
  timestamps, messageCount, preview, alreadyImportedSessionId) discovered
  in the local Claude Code / Codex / opencode stores.
- import_sessions: takes { selections: [{ tool, sourceId }] }, validates
  each selection against the known tool list, imports sequentially
  (per-session transactional), and broadcasts session_import_progress
  events ({ index, total, result }) so the UI can render live progress.
  Returns { results } with per-item ok/sessionId/title/error.

* feat(desktop): import sessions UI for Claude Code, Codex, and opencode

Adds an Import Sessions dialog to the desktop app driven by the sidecar's
list_importable_sessions / import_sessions commands:

- Scan phase discovers local history from all three tools and groups it
  per tool with select-all checkboxes, per-row title, relative time,
  message count, and workspace folder; rows already imported are disabled
  and badged (idempotent re-open).
- Text filter across title, folder, and first-prompt preview.
- Import phase streams session_import_progress events into a progress bar
  and per-item result list; the dialog cannot be dismissed mid-import via
  overlay click. Done phase summarizes successes and lists failures with
  their error messages.
- Entry points: an Import button in the Sessions view header and an
  "Import sessions" row in Settings → General.
- use-session-history subscribes to session_import_progress so history
  refreshes no matter which surface started the import.
- Wire types live in webview/lib/session-import.ts (mirrors the core
  module's types so the client bundle never imports node-only code).

* fix(desktop): import dialog crash rendering session timestamps

formatRelativeTime takes a string (parseTimestamp calls .trim() on any
truthy value), but the import dialog passed the numeric updatedAtMs,
crashing the page with 'e.trim is not a function' as soon as scanned rows
rendered. Convert to an ISO string at the call site.

Slipped through because the webview has no typechecking anywhere:
tsconfig.dev.json excludes webview/ and next.config sets
typescript.ignoreBuildErrors, and the webview's own tsconfig currently
carries 64 pre-existing errors.

* feat(desktop): offer session import during onboarding

Adds an 'import' onboarding step between connect/github and done. The
step scans for importable Claude Code / Codex / opencode history on
entry and silently advances when nothing (new) is found or the scan
fails, so only people with actual history from other tools ever see it.
When sessions are found it summarizes the count and source tools, opens
the same ImportSessionsDialog used by the Sessions page for picking, and
flips to a confirmation state once at least one session imports. Skip is
always available, including while the scan is still running.

* fix(desktop): import dialog text overflow, collapsible sections, select all

- Titles no longer clip or push the row wide: they word-wrap up to two
  lines (line-clamp-2 + break-words, with min-w-0 down the flex chain so
  long unbroken Codex prompt titles can actually shrink); the meta line
  keeps time/count fixed and truncates only the workspace name; progress
  rows get the same min-w-0 treatment.
- Each tool section header is now a collapse toggle (chevron +
  aria-expanded) so one tool with hundreds of sessions doesn't force
  scrolling past it; collapsed headers still show count and selected
  count, and filtering forces sections open so search matches can't hide
  in a collapsed group. Collapse state resets per dialog open.
- New global Select all row above the list with indeterminate state and
  an x-of-y selected counter; it operates on the currently visible
  (filtered) selectable sessions, matching the per-section checkboxes.

* fix(desktop): import dialog header and search clipped by intrinsic column width

The dialog grid used the default auto column track, so a single
unbreakable string in a session title (Codex titles often contain URLs)
set the column's min-content width wider than the fixed 620px dialog --
break-words affects layout but not intrinsic sizing -- and
overflow-hidden then clipped everything in the column, including the
description and the search field. Pin the column to minmax(0,1fr) so the
container width always wins and long words wrap at the box edge instead.

Also add sm:max-w-none (the primitive's sm:max-w-lg survives
tailwind-merge across variants and was silently capping the dialog at
512px) and shrink-0 on the search and select-all rows so a tall list can
never compress them vertically.

* fix(desktop): onboarding import step rescanned after import and looped to done screen

The import step's scan effect depended on onContinue, an inline arrow the
parent recreates every render — and importing itself re-renders the app
shell via the history refresh. Each re-render re-ran the scan, and when
the user had imported everything (select all), the re-scan found zero
remaining sessions and hit the nothing-to-import auto-advance, yanking
them past their own import confirmation onto the done screen. The scan
now runs exactly once per step entry (onContinue held in a ref for the
async auto-skip paths).

Also, after a successful import the button is now 'Start building' and
completes onboarding directly instead of routing through the separate
done screen — two consecutive confirmation screens read as a loop. The
skip and nothing-found paths still go through the done screen so those
users get the 'You're all set' confirmation.

* fix(core): consolidate imported tool_results into the message after their tool_use

The import sanitizer answered missing tool_use ids with a separate
placeholder user message while leaving real results for the same turn in
later user messages. Anthropic requires every tool_result for a turn in
the user message immediately following it, so a partially-answered turn
would still 400 on resume. Rebuild any incomplete or split span as one
consolidated results message in tool_use order (placeholders for missing
ids, duplicates dropped) followed by a message carrying whatever else the
span held, mirroring the legacy migration sanitizer.

* fix(desktop): imported sessions resume on the user's configured provider; batch adapter caches

Opening a history session adopts the row's provider/model
(use-chat-session: session.provider || prev.provider), so imported rows
stamped with the source tool's provider — openai-native for Codex,
whatever opencode reported — resumed on providers the user may never have
configured and failed on first send. The dialog now passes the app's
current model selection (lastProvider/lastModelByProvider, i.e. what a
new chat would run on) and the service stamps it on the row; both halves
must be present so a Cline provider is never paired with a foreign model
id. The source provider/model are preserved in metadata.importedFrom and
per-message modelInfo stays accurate. Codex's provider id is corrected to
Cline's openai-native, and opencode's openai/google map to
openai-native/gemini.

Adapters also gain per-batch caches released via dispose(): Codex's
convert() re-walked the sessions tree and re-read every rollout head per
imported session (O(sessions x files)); it now builds the session-id ->
richest-file index once per batch. opencode copied the whole WAL db per
imported session; it now snapshots once per batch.

* fix(core): roll back failed imports and dedupe at import time

Addresses both Greptile P1s on #13744:

- A write failing after createRootSessionWithArtifacts (messages, status,
  manifest, title) left a half-written pid-0 session in history whose
  importedFrom marker also blocked retrying the source. persistConverted
  now deletes the session on any later failure and rethrows.
- Dedup markers were read through listSessions, which caps its scan at
  2000 rows, so a prior import older than the newest 2000 sessions was
  invisible and the source could be imported again. Add
  listSessionMetadata (ids + metadata for every row, no manifest reads or
  reconciliation) and use it for markers. Also check idempotency at
  import time, not only at discovery: a request for an already-imported
  source resolves to the existing session (alreadyImported: true) instead
  of writing a copy, covering stale pickers and repeated requests.

* fix(core): create imported sessions terminal and mark them imported last

Two failure modes shared one root cause -- the import wrote its session
in stages and claimed success too early:

- The row was created running/pid-0 and flipped to completed afterwards.
  The stale-session reconciler runs in the hub daemon against the same
  SQLite DB and, in that window, marks such rows failed and stamps
  terminal_marker metadata. createRootSessionWithArtifacts now accepts
  status/endedAt/exitCode so imports are created completed with the
  source session's end time; the separate status flip and manifest
  rewrite are gone.
- The importedFrom marker was written at creation, so a session whose
  later writes failed (and whose rollback delete also failed) still
  blocked retrying its source. The marker is now the final write, so it
  means 'this import finished' and a half-written session can never
  claim the source.

listSessionMetadata is unbounded by default so dedup sees every row.

* fix(core): resolve TS2352 casts in session-import tests (#13746)

tsc rejects casting ContentBlock[] straight to Record<string, unknown>[]
(RedactedThinkingContent is not comparable), which failed the Quality
Checks typecheck. Route the five assertion-site casts through a small
blocks() helper that widens via unknown.

* fix(core): flatten Codex content-block tool outputs during import

Newer Codex rollouts write custom_tool_call_output.output as an array of
Responses-API content blocks ({type:"input_text", text}) instead of a
plain string. The importer JSON.stringified that array into the
tool_result content, and the chat UI's tool-summary parser then rendered
each non-text block as its type label, so imported exec calls showed up
as "[input_text][input_text]" with no output.

Concatenate the text of string/text-bearing blocks (they are stream
chunks, so no separator) and keep the JSON fallback for anything else.

* fix(desktop): edit-and-resend on runs without a checkpoint

Editing a message forks the session before that run, and the sidecar
always routed that through manager.restore with workspace: true. Imported
sessions carry no checkpoint history, so editing any of their prompts
failed with "No checkpoint found at or before run N" — even after the
user had continued the session in Cline, since only the new runs get
checkpoints.

When no checkpoint exists at or before the edited run there is no
workspace state to roll back, so fork the trimmed transcript onto the
current workspace (the same path a full-history fork takes) instead of
erroring. Runs that do have a checkpoint still restore the workspace.

* fix(core): roll back failed session creation and coalesce overlapping imports

Two gaps Greptile flagged on the import path:

createRootSessionWithArtifacts upserts the row before writing the messages
file and manifest, and the call sat above persistConverted's rollback try.
A file write failing there left a completed row with no transcript in
history. Creation now runs inside the rollback, and deleteSession already
tolerates a missing row or missing files.

Each import_sessions request builds its own service and snapshots the
existing-import markers once, so two overlapping requests for one source
(a second window, a double-fired command) both passed the dedupe check and
persisted two sessions. A module-level in-flight map keyed by tool:sourceId
makes the later caller wait on the first write and report its session as
already imported.

* fix(desktop): resolve the import resume target like a new chat does

An imported Claude Code session resumed on the Anthropic provider instead
of the user's Cline selection. The dialog read model-selection storage
directly and required both a remembered provider and a remembered model;
the composer only records a model from the explicit picker handlers, so
anyone running on the default model has no entry, the lookup came back
empty, and the service fell back to the source tool's provider.

Resolve the target with getInitialChatConfig() -- the same chain a new
chat uses (remembered selection, then the built-in default), which is
never empty -- and have the import_sessions handler default to the cline
provider and CLINE_DEFAULT_MODEL_ID when a caller sends nothing, matching
other server-started sessions. The source provider can no longer become
the resume target.

* feat(desktop): group scheduled runs under their schedule in the sidebar (#13752)

* feat(core): stamp schedule id, name, and run number onto scheduled sessions

Sessions started by the cron runner only carried a generic
sessionHistoryOrigin.trigger = "hub-schedule", so clients could tell a
session was scheduled but not which schedule it belonged to or which run
it was. The runner now passes schedule provenance to the runtime handlers,
which merge it into the session metadata alongside the origin trigger:

  scheduleId          the hub schedule's external id
  scheduleName        the schedule title
  scheduleExecutionId the cron run id
  scheduleRunNumber   1-based position among every run created for the spec

The run number comes from a new SqliteCronStore.getRunOrdinal, which counts
runs of every status in creation order so a later cancellation never shifts
numbers already stamped onto earlier sessions. A reclaimed run keeps its
number, so two sessions with the same number make a duplicate visible.

HubScheduleRuntimeHandlers.startSession gains an optional second argument
carrying the metadata; existing implementations that ignore it keep working.

* feat(desktop): group scheduled runs under their schedule in the sidebar

A schedule that fires daily filled the sidebar's Scheduled section with a
row per run, each titled with the same prompt text, which read as if the
task had been duplicated. Runs of one schedule now fold into a single
collapsible row named after the schedule, with the run count on the right;
expanding it lists the runs as "Run N" sub-items (newest first) with their
usual status dot, time, hover card, context menu, and delete button. The
group holding the active session expands on its own so a run opened from
the Schedules page is visible. Grouping also applies inside project groups
when sorting by project. The Scheduled header now counts schedules rather
than runs.

Threads learn the schedule identity from the metadata the runner now
stamps (scheduleId, scheduleName, scheduleRunNumber). Runs recorded before
that fall back to the schedule executions list the hook already polls,
which now yields the schedule id and name instead of a bare session id set,
and finally to grouping by shared title. Runs without a number are labelled
with their start time instead of "Run N".

* fix(desktop): reopen a collapsed schedule group when one of its runs is opened

A stored collapse used to win over the active-session default for the
sidebar's lifetime, so a run opened from the Schedules settings page
could stay hidden inside its collapsed group. Opening a session now
clears the stored choice for the group that holds it; the group can
still be collapsed afterwards.

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

---------

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

* feat(cli): handle outdated hub sessions with drain and replace flow (#13727)

* feat(cli): handle outdated hub sessions with drain and replace flow

Add logic to detect when the CLI is newer than the running Hub and provide
users with options to either keep the older Hub running (to avoid
interrupting active sessions from other clients) or force-replace it.

Implement `describeOutdatedHubSessions` helper to show quantified session
activity in the dialog, and add `HubOutdatedContent` UI component with
detailed messaging for the `build_mismatch` case. The `unsupported_protocol`
case remains a modal requiring update, while the softer mismatch now uses
a toast with enter-to-replace or escape-to-keep choices.

Includes tests for draining and replacing an older busy hub when forced.

* fix(hub): gate desktop hub_upgrade behind trusted connection and make drain-first a hard guarantee

Address review: an originless local WebSocket client could invoke the
forceful hub_upgrade command, and a failed drain request still allowed a
forced retirement, so work started during the wait window could be killed.

- hub_upgrade now requires the same canApproveTools per-connection gate as
  the tool-approval commands.
- upgradeManagedHub skips the idle-wait window when the drain was not
  established (an undrained hub keeps admitting work, so waiting only
  widens the blast radius) and refuses to replace a busy hub that did not
  accept the drain, force or not. An idle hub is still replaced so
  pre-drain-endpoint hubs (404) remain upgradable.

* fix(hub): treat failed activity readings as unknown, not idle, during hub upgrade

A transient session.list failure inside the drain wait window previously
read as an idle hub, which could end the grace window early and authorize
retirement while turns were still finishing.

- Failed readings never end the wait window early, never overwrite the
  last real observation, and never authorize a non-forced retirement.
- Without force, a hub whose activity was never confirmed is handed back
  un-drained (still_busy) instead of retired; an undrained hub is now
  replaced only when positively observed idle.
- With force and an accepted drain, an unanswerable hub is still replaced:
  the user already consented to interrupting its sessions.

* fix(hub): never retire an undrained hub on an idle snapshot

An older hub that rejects the drain has no admission barrier, so a single
idle reading cannot authorize retirement: a session admitted right after
the snapshot would die in a retire the consent prompt never covered.

upgradeManagedHub now retires a hub only under an accepted drain. The
undrained-idle case is delegated to the locked ensure path, which
re-checks activity immediately before its own retire ladder and attaches
(deferring the swap) when new work arrived in the meantime; the upgrade
then reports still_busy instead of replaced, and the desktop/TUI surfaces
tell the user to retry.

* fix(hub): require an accepted drain unconditionally before any upgrade retirement

Review follow-up: the undrained-idle delegation still reached
retireDiscoveredHub, whose own drain attempt is best-effort, so a session
admitted after the idle re-check could die in the shutdown.

upgradeManagedHub now fails fast when the hub does not accept the drain -
no wait window, no idle exception, no delegation. The drain is the
admission barrier that keeps every subsequent reading true through the
retire; a hub too old or wedged to accept it is left to the automatic
ensure path, which replaces it once idle at the next client startup, and
the error says so.

* fix(hub): establish the drain barrier before the automatic idle check

Review follow-up: the automatic incompatible-hub path read session
activity first and drained only inside the retire ladder, so a session
admitted between the idle snapshot and the shutdown could be terminated.

retireIncompatibleHub now requests the drain before the busy check: with
the drain accepted, the idle reading stays true through the retire. A
deferred (busy) hub, and one whose retirement fails or is skipped by the
circuit breaker, gets the drain lifted so it never sits alive-but-refusing
work. Hubs that do not accept the drain (pre-/drain builds answer 404)
keep the historical best-effort snapshot rather than being stranded
forever.

* polish(hub): tighten the outdated-hub dialog copy

Two short sentences instead of four long ones, spell out what Quit Cline
does (closes the app, leaves the Hub running), and rename the action to
Update Now in both the desktop dialog and the TUI variant.

* fix(cli): show the keep-Hub reminder toast when the outdated-hub dialog is dismissed (#13754)

dialog.choice() resolves undefined on Esc rather than rejecting, so the
reminder toast in .catch() never ran. Move it to the falsy branch of
.then(), matching the unsupported_protocol handler.

---------

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

* chore(sdk): release v0.0.82

* chore(cli): release v3.0.61

* fix(core): close imported-session stores before the temp dirs are removed

The session-import tests opened a SqliteSessionStore per case and never
closed it, so afterEach's rmSync ran against a directory still holding an
open SQLite file. POSIX allows that; Windows does not, and all seven
persisting cases failed the sdk-publish Windows job with EPERM on the
cline-db-* temp dir.

Route every store through a sessionStore() helper that registers it for
close, and close them before removing the dirs.

* chore(vscode): release v4.1.17 (#13755)

* chore(desktop): release v0.0.22

* fix(standalone): decode core-connection protobus requests from proto3 JSON (#13758)

The core connection delivers protobus requests as the proto3 JSON the
webview's ts-proto toJSON encoders produce: enums arrive as string names
and default-valued fields — empty repeated fields included — are omitted.
The handlers assume ts-proto message shapes (numeric enums, repeated
fields always present), so dispatching the parsed JSON directly broke
every RPC relying on those invariants on JetBrains: changing the API
provider threw 'Cannot read properties of undefined (reading length)'
in fromProtobufModelInfo, and the plan/act toggle rejected its own mode
as invalid. The old standalone gRPC server restored these defaults
during protobuf decoding; the tunnel skipped that step.

Generate a per-method request-decoder map (request type fromJSON)
alongside the service handlers and apply it in the core-connection
dispatcher before dispatch. The in-process VS Code webview path is
untouched: it posts structured-cloned ts-proto objects that never pass
through JSON.

* feat(core): Hub-managed Agent Plugins support (#13652)

* feat(sdk): add hub-managed Agent Plugins

* fix(sdk): restrict Agent Plugin auto-discovery

* test(sdk): canonicalize Windows plugin paths

* fix(sdk): await stdio MCP process shutdown

* fix(sdk): select Agent Plugin MCP clients by source

* fix(core): defer Agent Plugin data directory creation

* docs(sdk): clarify Agent Plugin discovery scope

* fix(sdk): reject individual Agent Plugin skill toggles

settings.toggle({type: "skills"}) unconditionally called
toggleSkillFrontmatter() for any resolved skill record, including ones
sourced from an Agent Plugin. That writes a `disabled` key into the
skill's SKILL.md frontmatter, but the strict Agent Skills parser used
for these skills only permits a closed field set (name, description,
license, compatibility, metadata, allowed-tools). The very next reload
then rejects the file as invalid and the skill silently disappears
until someone hand-edits the installed plugin's SKILL.md.

Guard the toggle: an agent-plugin-sourced skill record now throws a
clear error pointing at the plugin-level toggle instead, matching how
whole-plugin enable/disable already works (setDisabledAgentPlugin,
keyed by manifest name, no file mutation).

* fix(sdk): keep disposing MCP servers when one disconnect fails

InMemoryMcpManager.dispose() unregistered servers sequentially and let
the first disconnect() rejection abort the loop. Since disconnect() can
now reject when a stdio child never exits, one wedged server would leak
every remaining server's process. Catch per-server errors, disconnect
the rest, and rethrow as an AggregateError so upstream cleanup-error
reporting still sees the failure.

Also log agent plugin discovery failures in CoreSettingsService.list
instead of swallowing them silently, so a plugin missing from settings
is diagnosable.

* feat(cli): manage Agent Plugins through the Hub (#13657)

* feat(desktop): manage Agent Plugins through the Hub (#13658)

* feat(desktop): manage Agent Plugins through the Hub

* fix(desktop): show Agent Plugin inventory

* docs: add deprecation notices page (#13458)

* docs: add deprecation notices page

* docs: add primary surface to deprecations

* chore: drop unrelated formatting changes from docs PR

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>

* fix(desktop): harden main sync integrations

* style: normalize sync conflict formatting

* revert: preserve repository formatter conventions

* style: satisfy VS Code merge formatting

* chore(desktop): bump beta to 0.0.23-beta.1

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: JasmineLCY <38378321+JasmineLCY@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Haley Park <haleypark.design@gmail.com>
Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>
Co-authored-by: yzxcj797 <54314860+yzxcj797@users.noreply.github.com>
Co-authored-by: yzxcj797 <yzxcj797@users.noreply.github.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: 𝓜𝓲𝓼𝓼𝓪𝓻𝓲 𝓐𝓱𝓲𝓵 🌿 <143264692+missarii@users.noreply.github.com>
Co-authored-by: Dominic Cooney <dominic.cooney@cline.bot>
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Harrison <harrison@cline.bot>
Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: TheRealSpencer <32678829+TheRealSpencer@users.noreply.github.com>
Co-authored-by: Etisha Garg <etisha.garg@cline.bot>
2026-09-02 18:23:36 -07:00
+15 9f78f597ba chore(desktop): sync final main fixes before beta release (#13789)
* fix(vscode): continue the surviving session on resume instead of rebuilding with the original task text (#13175)

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilt the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

The preserved conversation history is the source of truth on resume, so
the fallback prompt now just asks the model to reassess the history and
continue, matching the legacy resume prompt which also never resent the
original task. User-typed text still takes precedence when provided.

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

* fix(vscode): continue the surviving idle session on follow-ups instead of rebuilding

Stopping a turn keeps the session alive, but every idle follow-up (bare
Resume after Stop, and typed follow-ups after a completed turn) tore
that session down and rebuilt it from persisted task history before
sending. Continue the matching idle session in place instead, the same
way the CLI reuses the live session after an abort. Rebuilding from
history now only happens when no live session matches the displayed
task (task opened from history, extension host reload).

A bare resume still needs a prompt to start a turn, so it sends the
neutral [TASK RESUMPTION] prompt (shared with the rebuild fallback and
hidden from the transcript); user-typed content is echoed and sent
as-is. If the send lands while the abort is still settling, the runtime
auto-queues it and drains once the abort completes.

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

* refactor(vscode): consolidate follow-up send paths in SdkFollowupCoordinator

Now that idle follow-ups continue the live session in place, the
two-mode sendToActiveSession helper was redundant: its non-queued branch
duplicated continueIdleSession minus the bare-resume prompt. Split it
into a single-purpose queueToActiveSession and fold the idle no-task
send into continueIdleSession, flattening askResponse's decision tree
to: queue onto a running turn, continue a matching live idle session,
rebuild from history, or abandon.

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

* refactor(vscode): reuse the existing neutral resumption prompt for bare resumes

Drop the newly invented long resumption wording in favor of the phrase
that already existed as the no-history fallback and that the transcript
hiding logic and test fixtures recognize: '[TASK RESUMPTION] Please
continue where you left off.' The net change to resumeSessionFromTask
against main is now just deleting the branch that resubmitted
historyItem.task as new instructions.

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

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilds the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

Bare resumes now always use the neutral prompt that already existed as
the no-history fallback; user-typed text still takes precedence. This
matches the legacy resume prompt (responses.taskResumption), which only
ever included user-supplied text as new instructions.

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

* fix(vscode): hide synthetic prompts from the queued-prompt echo

A send that races a settling abort is auto-queued by the runtime, so a
bare Resume can reach the pending_prompt_submitted echo carrying the
synthetic [TASK RESUMPTION] prompt. Echoing it leaked model-facing text
as a visible user bubble and shifted the visible-user-message ordinals
that edit/regenerate mapping relies on. Filter synthetic prompts with
isSyntheticUserPrompt, keeping user attachments visible (matching
isSyntheticSdkUserMessage semantics).

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

---------

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

* fix(vscode): preserve LiteLLM input token limits (#13293)

* fix(vscode): preserve LiteLLM input token limits

* fix(vscode): prefer live LiteLLM model metadata

* fix(vscode): generalize private catalog metadata

* test(vscode): preserve llms exports in vscode lm mock

* fix(vscode): point provider signup URLs at their API key pages (#13337)

* fix(vscode): point Mistral signup URL at the general API keys console

The Mistral provider's signup link led to the Codestral console, which
issues Codestral-scoped keys that fail with 401 on api.mistral.ai — the
endpoint the provider actually calls. Point it at the general API keys
page instead.

Fixes #13288

* fix(vscode): deep-link DeepSeek and Fireworks signup URLs to their API key pages

Both pointed at marketing homepages; link straight to the key-creation
pages instead, matching the rest of the registry and the desktop app's
provider-key-urls map.

* fix(ci): always build the legacy bundle from the legacy-extension branch (#13349)

The combined-VSIX workflow took legacy-ref as a free-form dispatch input
with no publish-time validation (next-ref has one: publish requires main).
Any typed ref — a PR merge ref, an unprotected branch — would be built
into the published VSIX by the environment-less build job, and the publish
environment approver only ever sees an opaque prebuilt artifact, so the
approval protected the marketplace PAT but not the shipped bytes.

Remove the input entirely and hardcode the protected legacy-extension
branch, which makes that branch's protection rules load-bearing for
releases. The tested-sha pinning between test-legacy and build is
unchanged. publish-extension skill dispatch command updated to match.

* fix(ci): lock the legacy publish workflow to the legacy-extension branch (#13350)

The branch dispatch input was a free-form string with no validation. Both
jobs checked it out and ran full npm lifecycle scripts from it: the publish
job next to VSCE_PAT/OVSX_PAT (and npm run publish:marketplace executes a
script from that same ref with the PATs in env), and the test job with NO
environment approval at all while inheriting the workflow-level
contents/packages/checks/pull-requests write grants. A dispatch pointing at
e.g. refs/pull/N/head would run outside-contributor code with the
marketplace keys behind one approval, or with a repo-write token behind
none.

Remove the input and hardcode the protected legacy-extension branch, drop
the workflow-level permissions to contents: read, and elevate only the
publish job to contents: write (tag push + GitHub release). The branch
input's default was legacy-extension, so normal publishes are unchanged.
publish-extension skill dispatch command updated to match.

* fix(vscode): SDK remote-config parity — refresh coordination, session gating, and fail-closed opt-out (#13226)

* feat(desktop): native notifications (#13166)

* feat(desktop): native notifications

* macos target

* fix(desktop): isolate macOS dev app identity

* fix(desktop): address notification review feedback

---------

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

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched (#13310)

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched

Toggling an auto-approve setting while a task is open writes
autoApprovalSettings into the StateManager's task-settings overlay
(updateAutoApprovalSettings -> setTaskSettings). The SDK controller never
cleared that overlay on clearTask/showTaskWithId (the legacy controller
did), so after New Task the stale overlay kept shadowing global settings
in getGlobalSettingsKey(): toggle RPCs were accepted into global state,
but every posted state still carried the overlay's old version, which the
webview rejects as not newer - the auto-approve checkboxes froze forever.

Restore legacy parity in SdkTaskControlCoordinator: drop the overlay
(persisting pending writes first) in clearTask() and before installing a
different task's proxy in showTaskWithId().

Fixes #13260

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

* changeset

* test(vscode): add end-to-end regression test for auto-approve freeze after New Task

Wires the real StateManager, the real updateAutoApprovalSettings handler,
and the real SdkTaskControlCoordinator.clearTask() together with the
webview's version gate modeled on ExtensionStateContext, pinning the
end-to-end invariant behind #13260: checkbox toggles must keep reaching
the webview after a mid-task toggle followed by New Task. Verified the
test fails when the clearTaskSettings() call is removed from clearTask().

* fix implicit any in regression test

---------

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

* feat(desktop): show provider web-search support under the settings toggle (#13328)

* feat(desktop): show provider web-search support under the settings toggle

The global Web search toggle silently does nothing unless the session's
provider offers native web search, which made the setting read as if it
worked with any provider. The desktop General settings row now explains
that only providers with built-in web search honor it, and shows a live
status line: which connected providers are ready to use it (no extra
setup needed), or an amber warning with a link to the Models section
when none of them support it.

Support is resolved in the webview via a new providerOffersModelTool
helper in @cline/llms (browser export), sharing the same builtin-manifest
source of truth as the runtime's supportsModelTool attachment check.

* fix(desktop): address review — refetch web-search status on catalog invalidation, clarify per-model support

Greptile P2: the one-time catalog fetch could race an in-flight provider
save and show stale status; the row now refetches when the provider
catalog cache is invalidated (fired after saves complete).

Greptile P1: the ready line implied every model on the provider works;
Vertex excludes Claude routes, so the copy now scopes the promise to
models that support it.

---------

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

* feat(ui/desktop): collapse finished runs into a work summary and remove hover-state dead space (#13315)

* feat(ui): add WorkActivity collapsed-run summary and float message actions as a pill

WorkActivity/WorkActivityTrigger/WorkActivityContent fold a finished agent
run's working rows (tool calls, thinking traces, narration) behind a single
"Worked for 4m 12s · 14 tool calls" disclosure built on the shared animated
disclosure primitives, with formatWorkActivityLabel/formatWorkDuration
exported for consumers.

Message hover actions no longer rely on the transcript reserving blank space
below each message: the action row is now a self-backed pill (border,
blurred background, shadow) that floats over whatever follows, so
conversations can pack rows tightly without hover chrome colliding with the
next message.

* feat(desktop): collapse finished runs into a work summary and tighten chat spacing

collapseCompletedWork post-processes the grouped transcript: once a run ends
on assistant text with no further tool calls, its working rows fold into one
expandable WorkActivity row while the final answer stays visible. Runs are
delimited by user messages; the trailing run only collapses when the session
has stopped running and actually produced an answer, so live streams and
cancelled/failed tails keep their rows. Assistant messages carrying images
or media are treated as deliverables and never collapse.

The conversation list gap drops from gap-8 to gap-4 now that hover actions
are self-backed pills that need no reserved space, and user messages add
their own top margin so turn boundaries stay visually distinct.

* refactor(ui/desktop): work summary label wording, flat expansion, stable in-run rhythm

Feedback round on #13315:

- Label reads "Worked for 4m 12s and made 14 tool calls" instead of joining
  with a dot; without a duration it falls back to "Made N tool calls".
- Expanded work rows render at transcript level — no rail or extra indent —
  since tool rows and thinking traces already carry their own nesting when
  expanded. The work content keeps the tight working-row rhythm.
- Live working rows (thinking traces + tool calls) now group into a 'run'
  render item with the same tight 0.25rem rhythm, so there is no oversized
  gap under a "Thought for Ns" row and every row keeps its exact position
  when the finished run folds into the work summary. A trailing
  answer-in-progress stays outside the group at transcript level, and pure
  prose spans keep normal spacing.
- The transient "Thinking..." indicator moves inside the transcript column
  and mirrors a trigger row's geometry, so the first real row replaces it in
  place with no jump.

* style(ui/desktop): hover-pill metrics, right-pointing work chevron, scroll and spacing fixes

Another feedback round on #13315:

- Hover action pill: +2px internal padding, a trailing inset after the
  timestamp (it sat flush against the pill border), and more clearance
  between the message content and the pill (2px -> 6px; the hover bridge
  grows to match).
- The work summary chevron points right while collapsed and continues
  counterclockwise to point up when expanded.
- Conversation bottom padding drops pb-20 -> pb-8: the composer sits below
  the scroller, so the padding only needs to clear a pinned action pill.
- Sending a message scrolls back to the bottom even if the reader had
  scrolled up (new AutoScrollOnSend on the user-message count, which ignores
  optimistic-bubble re-keying; @cline/ui now exports useConversation for
  this).
- An assistant answer directly under its run's working rows pulls itself
  0.5rem closer than the full transcript gap.

* style(desktop): leave a visible gap between a pinned action pill and the composer

pb-8 exactly matched the pill's ~40px footprint, so the last row's hover
actions sat flush against the composer top; pb-12 restores ~8px of daylight.

* style(desktop): widen the gap between the pinned action pill and the composer to ~24px

pb-12 left only ~8px of daylight under the pill; pb-16 reads comfortable
without reverting to pb-20's dead space.

* fix(desktop): keep the thinking indicator at the working-row offset mid-run

The indicator matched a trigger row's geometry but sat a full transcript gap
(1rem) below the last working row, while the tool/thinking row replacing it
joins the tight run group at 0.25rem — a visible upward jump. When the last
transcript item is working rows (or streamed assistant output), the
indicator now pulls up to the same tight offset; only at the start of a run,
under the user message, does it keep the normal gap.

* style(ui): calm the hover actions surface per team feedback

Borderless rectangle instead of the bordered pill: radius drops to
var(--radius), the side padding goes entirely (the icon buttons carry their
own hit areas), and the vertical padding halves. Blurred background and
shadow stay so it remains legible over following content.

* feat(ui/desktop): full-band hover reveal and iOS-style disclosure easing

The hover actions only appeared while the pointer was inside the message
box itself. The invisible bridge under each message now spans the full
height of the band the floating actions occupy (full row width), so
hovering anywhere in that strip reveals them. Sibling row types
(.cline-chat-tool, .cline-chat-work, and the desktop's run/tool groups)
become position: relative so they paint above the bridge — their own
content keeps its hover and clicks, and the bridge only wins in the band's
genuinely empty space.

All expandable rows (work summary, tool panels, thinking) open and close on
a 240ms symmetric ease-in-out cubic-bezier instead of the 60ms snap, with
chevron rotation on the same curve. Reduced-motion still disables both.

* revert(ui/desktop): drop the full-band hover reveal; quicken disclosure easing to 180ms

The full-band hover bridge (and the position: relative changes that made it
safe) is reverted per feedback — back to the narrow bridge that only spans
the gap under the message. The iOS-style ease-in-out on disclosures stays
but speeds up from 240ms to 180ms.

* fix(ui): recover live tool diffs that mount as a blank pierre skeleton

Live-streamed edit rows could show an empty diff for the whole run, with the
diff only appearing after the collapsed work row was expanded (fresh mount).
Root cause, confirmed by driving a live session and inspecting the element:
React StrictMode double-invokes @pierre/diffs' ref callback; the first
instance's async highlight work aborts on its immediate cleanup, and the
second instance adopts the abandoned half-rendered shadow tree as if it were
complete prerendered output — zero height, no code, no theme stylesheet,
permanently. A rendered diff always carries style[data-theme-css] in its
shadow root, so ToolFileDiff now checks for it shortly after mount and
remounts FileDiff (bounded attempts) when missing; the fresh host element
takes the normal render path and recovers within ~400ms. Verified live: the
diff now renders during the run.

* fix(desktop): keep interrupted runs expanded even with partial trailing text

The trailing-run collapse gated on 'ended with assistant text', which
misread a Stop that landed mid-answer as a finished run and folded the tool
calls the user wants to inspect. The gate is now the terminal status itself:
only completed (or restored-idle) sessions collapse the trailing run;
cancelled/failed/error tails keep their rows regardless of partial text.
(Greptile P1 on #13315 — matches the PR's stated rule.)

* feat(ui): share the markdown pipeline, chat polish, and ThinkingBlock across products (#13323)

* feat(ui): share the markdown pipeline, chat polish CSS, and ThinkingBlock

The desktop app and the cloud dashboard both consume @cline/ui yet rendered
assistant output differently, because Markdown policy and the thinking-trace
row lived app-side. This moves the shareable parts into the package:

- components/markdown (new export): the lazy Shiki code highlighter (GitHub
  light/dark, pinned language set) and agentMarkdownControls — the standard
  Streamdown configuration. streamdown/shiki/@shikijs/* become optional peer
  dependencies, mirroring @pierre/diffs.
- components/markdown.css: the desktop's chat polish moves in — chat-scale
  headings, outside list markers, single quiet code blocks with a
  hover-revealed copy control, table cards. Kept unlayered so it beats
  Streamdown's layered Tailwind utilities without !important.
- ThinkingBlock + formatThoughtLabel in agent-chat: the standard thinking
  row (brain icon, Thinking/Thought-for-Ns label, streaming shimmer, rail
  presentation, capped scrollable body). The shimmer and the
  reasoning-hover-suppression rule move into agent-chat.css; triggers gain
  the color transition the desktop applied locally.

Version bumps to 0.2.0-next.5 for the dashboard to pick up.

* refactor(desktop): consume shared markdown and thinking primitives from @cline/ui

The local Shiki highlighter, Streamdown controls, chat markdown polish CSS,
streaming-title shimmer, and reasoning hover-suppression rule are deleted in
favor of the @cline/ui versions (the highlighter test moves to the package's
suite). ReasoningBlock becomes a thin wrapper that hands MemoizedMarkdown to
the shared ThinkingBlock, and formatThoughtLabel re-exports from the package
so grouping code and tests keep their import path.

globals.css now imports @cline/ui/components/markdown.css (unlayered, so the
polish keeps beating Streamdown's layered utilities); the app keeps only what
is genuinely app-specific: link/image policy in markdown.tsx, selectability
rules, accent palettes, and the view-enter transition.

* style(ui/desktop): make thinking-trace prose legible

Thinking body text rendered too faint: plain muted-foreground plus the
desktop's font-thin weight. The shared thinking content now leans 75% of the
way back toward the body text color (still slightly de-emphasized), and the
desktop drops the thin font weight.

* ci(ui-publish): build @cline/shared before ui typecheck (#13354)

@cline/ui's generated-media imports @cline/shared/browser, which resolves to
shared's dist output. The build-shared step sat after typecheck/test/build,
so the first ui-publish dispatch since #13025 failed at Typecheck UI with
TS2307. Move the step to right after install.

* fix: run_commands object form without args routes through the shell instead of failing with ENOENT (#13336)

* fix: run_commands object form without args routes through the shell

The structured { command, args? } form of run_commands was always spawned
directly with shell: false. When a model emitted a full command line in
command with no args (e.g. { command: "echo hello" }), spawn failed with
ENOENT for any command containing a space, breaking command execution for
the whole session.

Direct exec now only applies when a non-empty args list is provided; the
object form without args is routed through getShellInvocation like the
string form. Schema descriptions are tightened so models put arguments in
args instead of embedding them in command.

Fixes #13279

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

* chore: trim structured-command schema descriptions

The union schema is only used for lenient validation of input the model
already sent; its descriptions never reach a model prompt. Keep them
short instead of restating executor behavior.

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

* chore: simplify direct-exec comment in shell executor

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

* revert: keep original structured-command schema description

The description never reaches a model prompt and the executor now handles
both shapes, so the wording change was cosmetic noise.

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

* fix: gate direct exec on args key presence, not array length

Review feedback: an explicit empty args array is intentionally structured
input and stays direct exec; only an object with no args key is treated
as a full shell command line. Matches the key-presence rule already used
by the VS Code host's formatCommandForTerminal. Also replaces the
empty-args shell test (which was PowerShell-incompatible) with a test
pinning the direct-exec contract.

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

---------

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

* fix: normalize Gemini custom base URLs for legacy host-root values (#13329)

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

* docs: add GLM-5.3 to ClinePass models and reference pricing (#13357)

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

* feat(desktop): stream run command output (#13179)

* feat(desktop): stream run command output

* fix(sdk): clean up detached command logs

* fix(sdk): reap detached logs after hub restarts

* fix(sdk): preserve live detached command logs

* fix(desktop): harden live command progress

* fix(sdk): recover detached logs for local hosts

* fix(desktop): reconcile command output tool rows

* fix(sdk): retain logs for surviving commands

* fix(core): prevent PID reuse from retaining detached logs

* fix(core): preserve detached logs on probe failures

* fix(core): retain detached logs during probe outages

* fix(desktop): resolve leftover merge conflict in messages projection test

Combine both sides of the assertion: main's incremented per-block
createdAt projection and this branch's toolCallId/hookEventName meta.

---------

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

* fix(cli): make TUI dialog colors follow theme changes live (#13355)

* fix(cli): make TUI dialog colors follow theme changes live

Dialog content previously read the static palette constant, so open
dialogs (including the theme picker itself) kept the default dark-blue
accents while scrolling through theme previews. Add getDialogPalette /
useDialogPalette, which resolve dialog colors from the active theme's
dialog accents and re-render on every theme change, and migrate all
dialog-rendered components to it.

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

* feat(cli): derive dialog panel background from the active theme

Dark themes now lift their own background one OKLAB step for the dialog
surface, so panels keep the theme's hue instead of the library's fixed
#262626. DialogThemeSync pushes the surface into the dialog container
for new dialogs and repaints open panels, so the surface also follows
live theme previews. Light themes keep the neutral dark panel to match
the dark accent fallback and the light-on-dark dialog text.

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

---------

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

* fix: skill slash commands load via the skills tool instead of expanding into the user message (#13327)

* fix(desktop): show typed slash command instead of expanded skill markdown

The sidecar expands /skill and /workflow tokens into their instructions
before dispatching, so the runtime's persisted transcript only contains the
expanded text. After a turn (and when reopening a session) the webview
re-hydrates from that history and rendered the whole SKILL.md body as the
user's message; queue events echoing the expanded prompt could also add a
second user bubble, and fresh sessions were titled with the markdown's first
line. The CLI never shows this because its TUI keeps the typed text in its
own transcript and only sends the expanded prompt to the model.

Mirror that separation inside the desktop sidecar's display boundaries:

- history projection (readSessionMessages) inverts user text that starts
  with a configured command's instructions back to '/name remainder',
  which also repairs sessions recorded before this fix
- queue snapshots and chat_queued_prompt_start events echo the typed
  prompt recorded at expansion time, so the webview's optimistic-bubble
  re-key matches again
- an untitled session sent an expanded prompt gets titled from the typed
  command instead of the instructions' first line

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

* fix(desktop): don't overwrite a mid-turn rename with the typed-command title

The untitled check ran before dispatch, so renaming a fresh slash-command
session while its first turn was running got clobbered by the post-turn
typed-command title. Re-check at write time and only replace a missing title
or the one the runtime auto-derived from the expanded prompt.

Also documents the inherent prefix-inversion ambiguity flagged in review:
text hand-typed with a command's exact instructions persists byte-identically
to that command's expansion, so stored history alone cannot distinguish them.

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

* fix(desktop): stop expanding skill commands; let the skills tool load them

Pasting the skill body into the prompt is why the transcript could ever show
it: the desktop webview re-hydrates from the runtime's persisted history, so
whatever the sidecar splices into the user message renders as if the user
typed it. The runtime already registers the skills tool, whose description
requires the model to invoke it whenever the user references a slash command
— so send the typed /skill text through and let the tool deliver the
instructions as a tool result (previously they arrived twice: pasted and via
the tool). The persisted user message, session title, and queue entries are
then simply the typed command, which deletes the typed-prompt registry, the
queue event/snapshot rewriting, and the title machinery from the previous
approach.

Workflows are not served by the skills tool and keep textual expansion, so
the read-time display inverter stays: it collapses expanded workflow prompts
— and skill prompts persisted before this change — back to the typed
/command in the history projection.

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

* feat(core): option to keep skill slash commands typed for the skills tool

resolveRuntimeSlashCommandFromWatcher (and the hub snapshot proxy) accept
expandSkillCommands: hosts whose sessions register the skills tool pass
false so the typed /skill goes through and the model loads the instructions
as a tool result, keeping the persisted transcript as what the user typed.
Workflows always expand — the tool does not serve them. isSkillsToolAvailable
exposes the catalog check hosts use to decide (yolo preset and the skills
tool toggle leave textual expansion as the only delivery path).

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

* fix(cli): skill slash commands load via the skills tool instead of expanding

The TUI user-command wrap and buildUserInputMessage now keep a typed /skill
as-is when the session's mode/toggles register the skills tool, matching the
desktop app; workflows keep expanding, and yolo (zen) keeps expanding skills
because its preset has no skills tool. This also fixes CLI resume/history
surfaces showing the skill body: the persisted user message is now the typed
command.

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

* fix(vscode): keep configured skill slash commands typed for the skills tool

expandSlashCommands no longer splices a configured skill's instructions into
the model text; the SDK session's skills tool delivers them as a tool result
(previously they arrived twice). Builtin pseudo-skills like /deep-planning
are not served by that tool and keep expanding, as do workflows.

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

* refactor(desktop): use the shared skill-expansion option in the sidecar

Replaces the sidecar's workflow-detection dance with core's
expandSkillCommands option and gates on isSkillsToolAvailable, restoring
textual expansion where the tool is missing (yolo mode or the skills tool
toggle) — a gap in the previous desktop-only change.

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

* refactor(desktop): drop the display inverter for expanded transcripts

Accepted trade-off to keep the change minimal: sessions recorded before
skills switched to the skills tool, workflow sends (deprecated), and
yolo-mode skill sends persist expanded instructions and now render that text
as-is instead of being collapsed back to the typed /command at projection
time.

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>

* Use fixed selection chevron in account dialog to match other dialogs (#13364)

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

* fix(desktop): align system prompt with session mode (#13361)

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

* fix(desktop): finalize queued turns on chat_done with canonical history reconcile (#13330)

Turns that settle through the event stream (queued prompts, including the
first prompt of a fresh session) resolve their send() RPC early, so nothing
cleared the streaming shimmer or reconciled live-streamed content against
the persisted transcript at turn end. A turn whose deltas were incomplete
stayed visually streaming forever and only healed when a later non-queued
send rehydrated history.

chat_done (and chat_session_ended / the queue-drain double check) now clears
the active assistant streaming id and schedules a short-delayed
read_session_messages + applyCanonicalHistory, guarded by turn epoch,
session id, and in-flight send submissions so it never clobbers a newer
turn or duplicates the blocking send path's own finalization.

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

* chore(desktop): release v0.0.14

* fix(clients): filter non-chat models from chat pickers (#13317)

* fix(clients): filter non-chat models from chat pickers

* fix(clients): align chat model eligibility

* fix(desktop): strip user_input envelope when copying a user message (#13369)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>

* docs: show DeepSeek V4 peak and off-peak pricing (#13312)

* docs: update DeepSeek V4 average pricing

* docs: show DeepSeek peak and off-peak pricing

* docs: add GLM-5.3 reference pricing (same as GLM-5.2)

* docs: add GLM-5.3 to ClinePass models table

* fix(llms): display billed gateway cost (#13385)

* fix(shared): run PowerShell commands with fail-fast error semantics (#13358)

* fix(shared): run PowerShell commands with fail-fast error semantics

The run_commands PowerShell wrapper never set $ErrorActionPreference, so
the default 'Continue' applied: a pipeline erroring per item (e.g. a
malformed Where-Object over Get-ChildItem -Recurse) emitted one error
record per enumerated file - tens of thousands of stderr records on
large trees, looking like a hang - and could still resolve as SUCCESS
with exit 0.

Prepend $ErrorActionPreference='Stop'; to the script content executed
by the ScriptBlock so the first error terminates the command with a
non-zero exit and a single error message. Concatenated on the same line
as the user command so error line numbers stay unshifted.

Fixes #13285

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

* fix(shared): set the fail-fast preference in the bootstrap scope

Setting $ErrorActionPreference='Stop' by string-prepending it into the
scriptblock source displaced a leading param(...) from its mandatory
first-statement position, so scripts beginning with a param block failed
with CommandNotFoundException. Preference variables are dynamically
scoped, so setting Stop in the -Command bootstrap gives the invoked
scriptblock identical fail-fast semantics while keeping the user script
byte-identical (param works, error positions unshifted) and drops the
doubled-quote escaping.

* docs(shared): document the fail-fast tradeoffs in the PowerShell wrapper

Stop promotes every non-terminating error, not only per-item pipeline
floods: partial-result commands (recursive listings over access-denied
junctions) now stop at their first error, and Windows PowerShell 5.1
turns in-script stderr redirection of succeeding native commands fatal.
State this in the wrapper comment as a deliberate tradeoff, with the
GitHub Actions precedent and the per-command opt-outs.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* ci: stop over-long changelogs from silently dropping release Slack posts (#12955)

Slack section blocks reject text longer than 3000 characters. The Slack
action logs that rejection as ##[error] but does not fail the step, so an
over-long changelog drops the release announcement while the run stays
green — cline@3.0.50 (3272 chars) published to npm, tagged, and cut a
GitHub release with no Slack post and nothing red to notice.

Every publish workflow pasted the changelog section verbatim into one
section block, so all six were exposed; the SDK, desktop, and extension
sections were only 150-350 chars under the ceiling.

Add a slack_content output alongside content: unchanged when the section
fits, otherwise trimmed on a line boundary with a link to the full
release notes. Only the Slack payload uses it — GitHub release bodies and
the desktop updater manifest still get the whole section.

* ci: tidy workflow cache config and job permissions (#13403)

Publish workflows now always do clean npm installs (no dependency
cache in their test gates), the e2e workflow's cache keys are
exact-match only, and the e2e job drops an id-token permission it
never used.

* Rename desktop app from "Cline Code" to "Cline" (#13401)

* Rename desktop app from Cline Code to Cline

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

* Format touched Rust test assertions

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

---------

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

* fix(llms): surface provider-executed tool activity as observational events (#13300)

* fix(llms): surface provider-executed tool activity as observational events

Provider-executed tool parts (e.g. every tool the Claude Code CLI runs
inside its own session) were dropped by the model-tool guard added for
web search: only declared model tools were re-emitted, everything else
hit continue with nothing yielded. Those sessions modified the workspace
with no tool activity in runtime events, transcripts, or the UI.

Route all providerExecuted parts onto the observational path instead:
emit execution-tagged tool-call-delta and tool-result events, matched by
tool-call ID for providers that omit the flag on the result half. They
stay out of AgentRuntime's execution/approval loop, and the runtime
already persists them as modelToolActivities and projects them for
display.

The AgentModelEvent tool-result variant widens toolName from
ModelToolName to string to carry the provider's own tool names.

* fix(agents): keep turns that are only provider-executed tool activity

A turn consisting solely of observational tool activity has an empty
assistant content array - the activity lives in message metadata, since
projecting it into content would replay tool_use blocks the model never
gets results for. The empty-content guard threw on such turns, erroring
the run and losing the activity from the transcript. Count model-tool
activity as content for the emptiness check (error finishes still
throw); replay stays safe through the codec's empty-content placeholder.
Also drop the trailing text delta from one gateway test so the tool-only
stream shape stays covered end to end.

* feat: allow agents to create scheduled tasks (#13331)

* feat(core, desktop): add durable todo agenda

* fix(desktop): secure todo approvals and track tool usage

* fix(desktop): clean up failed approval delivery

* fix(desktop): authenticate approval connections

* fix(desktop): cancel approvals on broadcast failure

* fix(desktop): authenticate development approvals

* fix(desktop): harden development approvals

* test(core): make task paths cross-platform

* fix(desktop): serialize approval readiness

* refactor(core): unify todo and schedule tools

* feat(core): distinguish user todos from agent suggestions

* fix(core): hide tasks tool in yolo mode

* fix(core): enforce schedule workspace scope

* fix(core): bind schedule scope to hub connection

* fix(core): establish task scope at hub startup

* fix(core): scope task automation by workspace

* test(core): normalize workspace path expectations

* test(core): serialize Windows CI workers

* fix(core): reject unregistered schedule authority

* fix(desktop): guard task execution commands

* fix(core): avoid polynomial regex in mention parsing

* fix(core): address schedule tool review feedback

* fix(core): bind websocket clients to hub workspace

* fix(core): flatten tasks tool input schema

* fix(core): authorize multi-workspace hub clients

* test(core): type hub transport authority mock

* fix(cli): register a workspace client for remote schedule commands (#13398)

---------

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

* fix(desktop): treat ClinePass as OAuth-managed in the chat credential gate (#13404)

* fix(desktop): treat ClinePass as OAuth-managed in chat credential gate

ClinePass shares the Cline account OAuth credentials (its auth handler
stores under the "cline" provider), so the webview never sees a plain
API key for it. The chat pre-flight check only exempted cline/oca/
openai-codex, so switching to ClinePass while signed in via OAuth
blocked with "Missing API key" even though the sidecar resolves the
stored access token fine (which is why the CLI worked).

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

* style: format helpers.test.ts with biome

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

---------

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

* fix(ui): stack code block lines when streamdown lineNumbers is off (#13412)

streamdown renders each Shiki token line as a bare inline span with no
newline text between non-empty lines, and only applies its block line
class when lineNumbers is on. With lineNumbers off (the desktop app's
config) every multi-line fenced block collapsed into one run-on line.
Make the direct line spans under code-block-body display: block in the
shared markdown.css; empty lines keep their height via their lone "\n"
child under white-space: pre.

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

* fix(desktop): work summary undercounts wall time when pre-tool thinking attaches to the answer (#13413)

* fix(desktop): anchor work summary duration on the answer row, not attached pre-tool reasoning

The collapsed 'Worked for Xs' row undercounted wall time whenever a turn's
assistant message contained thinking + tool_use with no narration text: the
canonical projection emitted the reasoning-only row after the tool row (both
stamped before the tool executed), the webview attached that row to the final
answer, and collapseCompletedWork used the answer's earliest attached
reasoning timestamp as the end anchor - excluding the entire tool execution
(e.g. 'Worked for 5s' for a turn with an 8s command).

- webview: end the work span at the answer row's own timestamp, clamped to
  the last collapsed row so a fallback answer bubble with a synthetic early
  timestamp cannot shrink the duration either
- sidecar: flush pending thinking before a tool_use row so rehydrated
  transcripts keep the live-stream order (thinking before its tool call) and
  pre-tool reasoning no longer rides on the next answer

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

* fix(desktop): keep interleaved thinking between the tool calls it separates

Address Greptile review: when one assistant message interleaves thinking
between multiple tool_use blocks, each reasoning segment now projects at its
own position (attached to a text row from its own segment when present,
otherwise as its own row) instead of merging into the first reasoning row,
which displayed later thinking before a tool call it actually followed.

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

---------

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

* fix(desktop): remove settings gear hover state while Account screen is open (#13408)

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

* fix(desktop): don't show "No sessions found" while session history is still loading (#13414)

* fix(desktop): don't show 'No sessions found' while session history is still loading

Replace the isLoadingHistory flag with hasLoadedHistory, set only once the
backend has actually answered a list_discovered_sessions request. The sidebar
and Sessions view now keep their loading state until that first definitive
response, so the empty-state copy can no longer appear while history is still
being fetched (or while a failed fetch is being retried).

Also retry a failed initial fetch on the 2s event cadence instead of stranding
the UI until the 12s periodic poll, which is what stretched the misleading
empty state to ~10 seconds after a webview reload when the websocket lost the
race with the page load.

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

* fix(desktop): stop history fast-retry from re-arming after hook unmount

A failed initial fetch that settles after the hook unmounted could schedule a
new retry timer after cleanup had already cleared the refs, leaving the
abandoned hook polling the backend every 2s. Guard scheduleRefresh with a
disposed ref set by the mount effect's cleanup.

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

---------

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

* Fix @ file mentions breaking on paths with spaces (#13391)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

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

* Fix import ordering in mentions test (biome organize imports)

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

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

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

* Normalize mention paths to posix separators for Windows

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

---------

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

* Fix code actions failing with "command not found" on VS Code 1.134 (#13402)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

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

* Fix import ordering in mentions test (biome organize imports)

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

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

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

* Normalize mention paths to posix separators for Windows

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

* Fix code actions failing with 'command not found' on VS Code 1.134

Code action commands carried arguments (expandedRange, diagnostics),
which routes them through VS Code's CommandsConverter cache. VS Code
1.134 disposes the cached entries before the clicked action executes,
so every lightbulb action failed with 'Actual command not found,
wanted to execute cline.addToChat'.

Drop the arguments so the command id is passed through directly, and
recover the context in the handler instead: getContextForCommand now
expands an empty selection by 3 surrounding lines (matching the old
provider behavior) and gathers document diagnostics intersecting the
range when none are passed explicitly.

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

* Scope gathered diagnostics to the selection/cursor

Match the old CodeActionContext.diagnostics behavior: only include
diagnostics intersecting the range the action was requested for, not
the surrounding lines the text gets expanded to.

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

---------

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

* Desktop: unify Plugins, MCP, and Skills into one Plugins hub with a dedicated Marketplace page (#13411)

* Unify desktop plugins, apps, MCP, and skills into one Plugins hub with a Browse directory mode

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

* Open the marketplace directory as a modal over the Plugins hub instead of swapping the page

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

* Rename directory to Marketplace: Browse Marketplace button, Marketplace modal title with icon, search placeholder

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

* Fix search input focus ring clipped by the Marketplace modal scroll container

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

* Address Greptile review: keep selected tag chip visible when its count drops to zero, and remount installed tab when a marketplace install completes after the modal closed

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

* Track marketplace modal mutation flag in a ref so a close click racing a queued render cannot skip the inventory remount

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

* Make Marketplace its own settings page under Customizations and restore Channels as a standalone page

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

* Remove icon from Marketplace page header for consistency with other settings pages

* Notify mounted inventory views when the marketplace invalidates the cache so late install completions refresh the Plugins hub

---------

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

* feat(desktop/ui): recommended and free model tiers in the composer model selector (#13410)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

---------

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

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK (#13415)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

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

* feat(desktop): recommended-feed badges and descriptions in provider settings (#13416)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* feat(desktop): recommended-feed badges and descriptions in provider settings

Review suggestion on #13410: the provider settings page has room for
more model detail than the composer's picker. The cline/cline-pass
provider cards now refresh their model list through
list_provider_models (the catalog snapshot deliberately skips the
recommended-feed overlay so the startup catalog fetch never blocks on
the feed) and render Recommended/Free tier badges plus feed tags (NEW)
next to the model name, with the model description underneath. The
refreshed list also surfaces the live entries instead of the bundled
snapshot.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

* fix(desktop): scope the settings featured model list to its provider and revision

The fetched featured list was unscoped component state: switching
between cline and cline-pass reused the component instance, so the
previous provider's models stayed visible while the new request was
pending (or forever, when it failed), and the retained copy shadowed
later provider.modelList updates — adding a second custom model
submitted the stale list as the complete configuration and dropped the
first addition.

The fetched list now only applies to the provider and modelList
revision it was fetched for (falling back to the catalog snapshot
otherwise and refetching on membership changes), and add-model submits
the union of the displayed and configured ids so an update can never
silently unconfigure existing entries.

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

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

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

* fix(ui): update packed-Tailwind smoke contract for the picker's max-h-64 (#13421)

The ui-publish smoke check pins a set of Tailwind candidates the packed
sources must emit; #13410 grew the SearchCombobox options list from
max-h-56 to max-h-64, so the publish run failed on the stale candidate.
All other pinned candidates verified against the current sources.

* feat(desktop): refresh app icons and branding (#13400)

* ci(vscode): upload E2E failure recordings from the right path (#13427)

The job sets working-directory: apps/vscode, but that default applies to run
steps only, not to `uses:` steps. Since #10961 moved the extension under apps/
and added that default, the artifact path has resolved against the repo root,
matched nothing, and every failing run logged "No files were found with the
provided path: test-results/playwright/" instead of uploading recordings.

Widen to test-results/ so Playwright's error-context snapshots ship alongside
the videos.

* fix(hooks): deliver tool hook contextModification to the model (#13297)

* fix(hooks): deliver tool hook contextModification to the model

On the next engine, a tool_call (PreToolUse) hook's contextModification
was parsed into HookControl.context and then silently dropped: the
runtime beforeTool/afterTool result contract had no channel for
injecting conversation context. Legacy consumed it (ToolExecutor /
ToolHookUtils pushed <hook_context> blocks into the next user turn), so
this was a regression of documented behavior.

- Add appendContext to AgentBeforeToolResult/AgentAfterToolResult.
- AgentRuntime collects appendContext across hooks during an
  iteration's tool executions and appends one <hook_context> user
  message after the tool results, keeping tool-result parts contiguous.
- Map HookControl.context into appendContext in both subprocess hook
  layers (skipped when the hook cancels, matching legacy, where the
  message doubled as the error).
- Truncate injected context at 50KB per hook output, matching legacy.
- Concatenate appendContext across merged hook layers.

tool_result (PostToolUse) hooks still run detached with stdout ignored;
making them blocking so their context can be collected is a follow-up.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): stamp tool identity on injected hook context blocks

Contexts are batched into one message after the tool results, and
parallel tool execution collects them in completion order, so position
alone cannot attribute a block to its tool call. Add tool_name and
tool_call_id attributes to each <hook_context> block.

* fix(hooks): sanitize hook context block markup

Attribute values (tool_name, tool_call_id) are stripped of quote/angle
characters and embedded </hook_context> closers in hook output are
neutralized, so neither provider-supplied ids nor hook text can corrupt
or spoof a block's stamped identity.

* fix(hooks): neutralize forged opening hook_context tags in hook output

The previous sanitization only neutralized closing tags, so hook output
could still open a forged <hook_context> block claiming another tool's
identity. Escape both opening and closing embedded tags with one rule.

* fix(hooks): hide injected hook context from user-facing transcripts

Stamp the injected hook-context user message with displayRole 'system'
(the compaction-summary convention) so it reaches the model but does
not render as a user bubble in live or replayed transcripts. Without
this, resuming a session showed the raw <hook_context> block as if the
user had typed it.

* fix(hooks): neutralize case-variant embedded hook_context tags

The tag-neutralization regex was case-sensitive, so hook output could
still smuggle a forged tag as <HOOK_CONTEXT>. Match case-insensitively.

* fix(vscode): map PreToolUse contextModification into runtime appendContext

The extension's hooks adapter bridged file hooks into the SDK runtime
but forwarded only cancel/errorMessage, so a PreToolUse hook's
contextModification never reached the model. Map it into the runtime's
appendContext channel; HookFactory already truncates it at 50KB.

* fix(vscode): hide hook-injected context from replayed transcripts

Live sessions never rendered the injected <hook_context> user message,
but session reload replayed it as a user bubble (and post-resume turns
kept doing so). Treat these messages as synthetic in the user-message
mapping: honor the displayRole 'system' stamp the runtime sets, with a
text-prefix guard for paths where metadata is unavailable. This also
keeps edit/regenerate ordinal mapping aligned with visible bubbles.

* fix(hooks): run file hooks through exactly one layer per host

The VS Code extension registered two independent hook execution layers:
its own hooks adapter (config.hooks) and the SDK core's file-hook
extension from the runtime bootstrap. When both discover the same hook
files, every hook executes twice per event — and with context injection
wired, each contextModification would be injected twice.

Add a 'hooks' runtime config extension kind (in the default set, so the
CLI keeps core file hooks unchanged) and gate the bootstrap's file-hook
extension on it. The extension excludes 'hooks' at session start, so
its adapter — which also provides the hook status UI and the
hooksEnabled setting — is its single execution path.

* fix(vscode): discover hooks from the session workspace, not only global state

Hook discovery read workspaceRoots from global state shared across
every Cline instance, so another window repointing it made workspace
hooks silently stop being discovered. With the extension's adapter now
the single hook execution layer, that meant no hooks at all.

HookFactory takes an optional sessionWorkspaceRoot and unions that
root's .clinerules/hooks into discovery (and into cwd resolution), fed
from the session config's cwd. Shared-state discovery still works, so
behavior in the single-window case is unchanged.

* fix(hooks): keep sanitized hook attribute values distinguishable

Replacing every markup delimiter with the same underscore could
collapse two tool call ids that differ only by such a character into
identical stamps. Escape each delimiter with a distinct token instead.

* fix(hooks): make hook attribute sanitization injective

Escaping the underscore itself turns the attribute escaping into a
uniquely decodable code, so no two distinct tool call ids can collapse
to the same sanitized stamp (previously an id containing a literal
escape token could collide with an id containing the delimiter).

* fix(vscode): reconstruct hook status rows when replaying transcripts

hook_status messages are emitted live but never persisted, so reloading
a session dropped every hook row. The injected <hook_context> blocks
carry the hook source and tool name, so the replay translator now
rebuilds a completed hook status row from each block. The injection is
also no longer treated as a user turn boundary, so the final turn's
completion retag is unaffected by it.

* fix(hooks): collect PostToolUse hook output and honor its control (#13298)

* fix(hooks): collect PostToolUse hook output and honor its control

tool_result (PostToolUse) hooks ran fire-and-forget with stdout
ignored, so their entire JSON output — contextModification and cancel —
was discarded. Legacy awaited PostToolUse, injected its
contextModification into the conversation, and honored cancel.

- Run tool_result hook commands blocking (same 120s default timeout as
  tool_call) in both the hook-config-file layer and the agent-hook
  subprocess layer.
- Map their output: cancel stops the run with the hook's error message
  as the reason; otherwise context is injected via afterTool
  appendContext.

This restores legacy blocking semantics: tool results now wait for
tool_result hooks, but only in sessions that have one configured.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): bound tool_result hook wait and isolate cancel reason

Address review findings:
- The agent-hook subprocess layer forwarded an unset timeoutMs
  unchanged, so a tool hook command that never exits would block the
  agent indefinitely. Default both tool_call and tool_result to the
  120s bound the hook-config-file layer already used.
- A cancelling hook's error message was folded into the same context
  field as other hooks' injectable context, so merging controls could
  leak unrelated hook context into the cancellation reason. Carry it as
  a separate cancelReason, and surface it as the stop reason for
  beforeTool cancels too.

* fix(hooks): prefer errorMessage as a cancelling hook's stop reason

When a cancelling hook returns both contextModification and
errorMessage, the context-first parse precedence made the injectable
context the cancel reason and discarded the actual error. Parse the two
fields separately: errorMessage wins as the cancel reason (matching
legacy), and a lone errorMessage still folds into injectable context
for non-cancelling hooks as before.

* fix(vscode): honor PostToolUse hook cancel and contextModification

The adapter awaited PostToolUse hooks but discarded their output
entirely. Map cancel to a stop control (with errorMessage as the
reason) and contextModification into the runtime appendContext channel,
matching the PreToolUse mapping and legacy semantics.

* fix(hooks): whitespace-only errorMessage no longer suppresses the cancel reason

A cancelling hook returning meaningful context alongside a blank
errorMessage lost both: the parsers selected the whitespace as the
reason and the result mappers trimmed it away. Require a non-blank
errorMessage before it wins, so context serves as the fallback reason.
Apply the same fallback in the extension adapter's stop mapping.

* fix(core): stop Windows CI worker crashes from the agenda spec watcher (#13428)

* fix(core): watch agenda task specs via the resolved long path

fs.watch on a path with 8.3 short components (e.g. C:\Users\RUNNER~1
temp dirs) trips a libuv assertion in fs-event.c on Windows and aborts
the whole process. Since the agenda task manager landed, every hub
server test spins up its spec watcher on such a path on hosted Windows
runners, killing the vitest worker and failing the sdk-test Windows job
on every branch. Resolve the specs dir with realpathSync.native before
watching so libuv only ever sees the long form.

* test(ui): stub ResizeObserver for @pierre/diffs in tool-diff tests

jsdom does not implement ResizeObserver, so every ToolFileDiff render
logged a ReferenceError from @pierre/diffs to stderr. Tests still
passed; this just silences the noise the same way the constructable
stylesheet shim does.

* fix(core): skip the agenda spec watcher when the dir does not resolve

Falling back to the raw path on realpath failure would reintroduce the
Windows short-path abort; log and go without the watcher instead.

* fix(vscode): honor the classic truncation range when migrating legacy tasks (#13419)

Classic Cline truncated long conversations by omitting an index range of
api_conversation_history from every API request (keep the first
user-assistant pair, drop everything through the range end, strip
orphaned tool_results from the first kept message). The range was
persisted on the history item while the full history stayed on disk.

legacyApiHistoryToSdkMessages ignored conversationHistoryDeletedRange
and converted the entire file, so resuming a migrated long task handed
the SDK an untruncated working context that could exceed the model's
context window by millions of tokens - every request failed with
'prompt is too long' and every compaction restarted from the full
history (#12996, confirmed by the reporter: the task was migrated from
an older version and broke after a restart, with each compaction
starting from ~3M tokens).

The migration now replays exactly what the classic extension sent:
slice out the deleted range and drop orphaned tool_results, mirroring
ContextManager.getTruncatedMessages (see origin/main). Malformed ranges
fall back to the full history (previous behavior).

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

* fix(vscode): show the diff edit view for multi-line edits in CRLF files (#13417)

The edit preview computed proposed content with an exact old_text match, but
the SDK executor normalizes old/new text to the file's own line endings before
matching (#12305) - reads strip CR, so models emit LF-only text even for CRLF
files. Any multi-line old_text in a CRLF file therefore failed the preview's
match: the diff edit view silently never opened while the executor applied the
edit. Single-line edits (no line break in old_text) were unaffected, which is
why the diff view appeared to trigger inconsistently.

Mirror the executor's EOL normalization (and its literal $-sequence insertion)
in the preview computation.

Fixes #13296

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

* fix(core): report truthful session status so desktop checkpoint restore stops wedging (#13418)

* fix(core): keep hub session status truthful across queue-drained turns

Queue-drained turns settle only through the event stream, but the hub
runtime host mistranslated their lifecycle in two ways:

- session.updated events carrying only a snapshot (persistence updates)
  defaulted the projected status to "running". When one trailed the
  final idle update after a turn, clients that track busy state from
  status events (the desktop sidecar's workspace restore gate) stayed
  busy forever. Use the snapshot's real status and emit nothing when
  neither source reports one.
- the per-run agent.done dedup was only reset by run.started, which the
  daemon-side queue drain never publishes, so a drained turn's done was
  swallowed as a duplicate of the previous turn's. Reset the dedup on
  session.pending_prompt_submitted, and suppress stale run.completed
  events that land inside a drained turn's window so they can neither
  emit a phantom done nor consume the drained turn's dedup slot.

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

* test(desktop): cover restore unlock after an event-settled queued turn

Exports the sidecar's core-session event handler so the queued-turn
lifecycle (busy via status events, cleared by the done agent event,
restore allowed afterwards) is testable end-to-end.

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

* fix(core): start interactive sessions without a prompt as idle

The runtime host reported every new session as "running" until its
first turn ended. Interactive hosts (the desktop app) start sessions
with no prompt and dispatch turns through separate send calls, so a
created-but-never-prompted session stayed "running" forever — wedging
clients that gate workspace operations (checkpoint restore, message
edit) on active turns.

Interactive no-prompt starts now begin idle, start emits the session's
actual status (resumed sessions no longer masquerade as running), and
markTurn* transitions keep tracking in-memory status for lazily
persisted sessions so the first turn still reports running -> idle.

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

* style: format hub-runtime-host test filter

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

* refactor: drop the drained-turn done bookkeeping, keep the minimal fix

The stuck restore is fully explained by the two status defects (fabricated
"running" from snapshot-only session.updated events, and never-prompted
interactive sessions reporting "running"). The done-dedup machinery for
queue-drained turns addressed a separate cosmetic gap (queued turns emit no
chat_done, pre-existing) and required fragile run-window heuristics, so it
is removed to keep this change reviewable. Sidecar test now settles the
queued turn through the status event, matching the shipped mechanism.

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

* docs(sdk): document the truthful session-status contract

---------

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

* chore(deps): update Langfuse packages and bump app versions (#13443)

* chore(deps): update Langfuse packages and bump app versions

Update @langfuse/otel to v5.10.1 and add @langfuse/vercel-ai-sdk v5.9.1 for improved observability with Vercel AI SDK.

Bump versions for @cline/code to 0.0.14 and @cline/ui to 0.2.0-next.6, updated via bun.lock.

Other Changes:
Added optional userId to AgentRuntimeConfig.
Propagated userId, sessionId, conversationId, runId, iteration, provider, and model context into AI SDK telemetry.
Added AI SDK 7 runtimeContext with explicit includeRuntimeContext.
Added stable OTEL_SERVICE_NAME=cline-sdk.
Added runtime metadata assertions in agent tests.

* add taskId

* Revert "add taskId"

This reverts commit f20d31d96d.

* docs: simplify Open Cline step in installing guide (#13405)

* docs: remove duplicate GLM-5.3 rows in ClinePass tables (#13449)

Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>

* chore(sdk): release v0.0.76

* chore(cli): release v3.0.56

* docs(cli): scope the v3.0.56 release notes to CLI-visible changes

* feat(desktop): interactive welcome hero graphic (#13399)

* feat(desktop): add interactive welcome hero

* feat(desktop): support composable welcome hero variants

* feat(desktop): reskin first-run onboarding (#13441)

* refactor: centralize client tool availability (#13451)

* chore(sdk): release v0.0.77

* docs(cli): drop the tasks tool from the v3.0.56 notes, it is desktop-only

* chore(vscode): prepare 4.1.11 release

* chore(desktop): release v0.0.15

* fix(vscode): remote config MCP settings (#13466)

* fix(vscode): enforce enterprise MCP controls on the Customize marketplace

The unified Customize marketplace replaced the old MCP marketplace
without carrying over enterprise remote-config enforcement: the catalog
RPC returned every MCP entry and installs were never policy-checked,
so orgs with mcpMarketplaceEnabled=false or an allowedMCPServers
allowlist saw (and could install) all marketplace MCP servers.

- Filter MCP entries out of getMarketplaceCatalog when the marketplace
  is disabled, and restrict entries to the allowlist when configured
  (matching entry id, display name, installed server name, or source
  repo URL, mirroring legacy GitHub-URL allowlist ids)
- Reject installMarketplaceEntry requests that violate the policy
- Map the published catalog's repo/homepage fields onto
  sourceUrl/homepageUrl so URL-based allowlists can match
- Update the enterprise MCP server controls docs

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

* refactor: simplify MCP marketplace policy enforcement

Fold the policy check into marketplace-helpers, drop the dedicated
test suite, and trim the docs edit to the strictly necessary line.

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

---------

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

* Treat an empty preserved capability list as unspecified when seeding tools (#13465)

* Treat an empty preserved capability list as unspecified when seeding tools

toSdkModelInfo guarded the tools seeding with a strict
preservedCapabilities === undefined check, but modelHasCapability —
the runtime's own reader — treats undefined AND length === 0 as
"unspecified". A custom OpenAI-Compatible model whose stored
capabilities field is a defined-but-empty array (a config carried over
from before the field existed, or one round-tripped through a boundary
that defaults it to []) skipped the seeding; the first boolean
projection to run afterwards (e.g. supportsReasoning) then populated
the array, the runtime gate read the non-empty, tool-less list as
authoritative, and every tool definition was silently dropped from the
session (#13463).

The guard now covers the empty array too, matching the reader's
unspecified semantics.

* test: satisfy the store's isModelInfo gate so the empty-capabilities case actually reaches knownModels

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

---------

Co-authored-by: yzxcj797 <yzxcj797@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>

* chore(vscode): prepare 4.1.12 release

* Add feature flags to the desktop app (#13289)

* Add feature flags to the app

* React to account updates

* Address comments

* use a per-app file

* fix: propagate Langfuse session telemetry (#13473)

* fix telemetry session propagation

* feat telemetry client version metadata

* fix(core): address Langfuse review feedback — hub client identity + delegated agent session grouping (#13475)

* fix(core): rebuild hub session client identity from request headers

Hub-backed sessions do not transport extensionContext (it is local-only),
so the daemon's runtime built traces without the clientName/clientVersion
metadata even though the hub client bakes X-CLIENT-TYPE / X-CLIENT-VERSION
into the session's provider headers. Reconstruct extensionContext.client
from those headers during local runtime bootstrap so hub-backed Langfuse
traces carry the same client identity as local runtimes, and the daemon's
header re-resolution stops clobbering the original X-CLIENT-TYPE.

* fix(core): propagate parent distinctId/sessionId to delegated agents

Delegated agents (spawned sub-agents, configured agents, teammates) were
built without distinctId and sessionId, so their Langfuse traces had no
userId or sessionId and did not group with the parent user or session.
Thread the host-resolved distinctId through RuntimeBuilderInput and the
root sessionId through the delegated-agent config provider, and copy both
onto the delegated AgentConfig.

---------

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

---------

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

* ci(vscode): make combined nightly manual-dispatch only

The PublishNightly environment gained required reviewers, so each cron
run parked on approval, held the workflow's concurrency group, and
silently cancelled every scheduled run queued behind it. 20 consecutive
scheduled nightlies died this way between 2026-07-31 and 2026-08-21;
the only nightlies that shipped in that window were manual dispatches.

Drop the cron rather than leave a trigger that cannot succeed unattended.

* feat(hub): add drain and upgrade commands with replay support (#13468)

* feat(hub): add drain and upgrade commands with replay support

* handles disconnection

* feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport

Completes the wiring the previous commits' primitives needed:
HubServerTransport gains isDraining(), hub.drain/hub.status/profile.get
command handling, and replayEventsAfter() (backed by the durable event
log), plus the sequence/sinceSequence wire types they depend on in
shared/hub.ts. run-queue-handlers.ts reads the active bot profile's
plugin roots when executing durable runs.

Also adds hub/profiles/: profile.json (identity/rules/plugins) ->
system prompt composition, --profile / CLINE_HUB_BOT_PROFILE
resolution, and the bundled cline-dad profile with its
cline_hub_support read-only diagnostics tool.

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

* Revert "feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport"

This reverts commit 6696d5d202.

* fix(hub): dedupe replayed events by eventId, not just sequence

HubEventLogStore.append() returns a new envelope stamped with a
sequence rather than mutating the input, so a pending approval
re-issued sequence-less by subscribe() (it predates any durable-log
append) and its later sequence-stamped copy from the durable log are
two different objects carrying the same eventId. The replay-then-live
buffer in browser-websocket.ts only deduped by sequence, so the
sequence-less copy's guard never tripped and it was delivered a second
time when the buffer flushed after replay.

Track delivered eventIds alongside the sequence cursor; eventId
survives the append/stamp round-trip unchanged, so this dedupes the
exact-same logical event regardless of which copy arrives first.

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

* fix(hub): wire drain, durable event log, and run queue into the live transport

CI on this branch failed bun run build:sdk: browser-websocket.ts,
client/index.ts, and hub-websocket-server.ts (already on this branch)
reference sequence/sinceSequence, HubServerTransport.isDraining(), and
the "hub.drain" command — but the commit that reverted bot profiles
out of this branch also reverted this wiring, since it shared a commit
with the profiles work. That wiring is a hub concern, not a
bot-profiles one; split it back out.

- shared/hub.ts: sequence/sinceSequence types, run.enqueue/run.list/
  hub.drain/hub.status/stream.replay capability, command, and event
  names. profile.get intentionally excluded — stays bot-profiles-only.
- context.ts: isDraining() on HubTransportContext. botProfile field
  intentionally excluded.
- hub-server-transport.ts: eventLog/runQueue fields and start/stop
  lifecycle, publish() appends to the durable log, handleCommand cases
  for run.enqueue/run.list/hub.drain/hub.status, drain-refusal check,
  replayEventsAfter()/lastEventSequence(). startBotProfile()/
  startHubSupportTool() and the profile.get case intentionally
  excluded.
- run-queue-handlers.ts: added without handleProfileGet (needs
  ctx.botProfile, which doesn't exist here).
- hub-upgrades.test.ts: added without its two bot-profile-injection
  tests (they need a resolved bot profile to assert against).

Verified bun run build:sdk exits 0 (the exact CI command) and
bunx vitest run src/hub passes (311/312; the one failure is the
same pre-existing environment-timing flake already present before
this change).

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

* fix(hub): export instance-lock, event-log, and run-queue from the hub barrel

These landed as internal modules only; hub-server-transport.ts and
hub-websocket-server.ts import them by direct path, but nothing
re-exported them from the public @cline/core/hub surface the way
sibling discovery/server modules already are.

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

* fix(hub): wire the instance lock into the daemon entry point

The singleton lock (discovery/instance-lock.ts) and its consumption in
startHubWebSocketServer/ensureHubWebSocketServer were already on this
branch, but the daemon entry point's own half was not: retrying a bind
when a retiring predecessor still holds the lock, and exiting with a
distinct code (3) instead of the generic fatal path when a live Hub
already owns the data directory. Without this, a daemon racing a
retiring predecessor could fail outright instead of waiting the lock
out, and losing the singleton race looked identical to a crash.

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

* fix(hub): address drain/upgrade review findings (#13478)

- cline hub upgrade: check idleness at least once (--wait 0 works), reject
  non-numeric --wait, and un-drain on every abort path so an aborted
  upgrade can never leave the hub refusing new work
- add cline hub drain --off and the off query param to requestHubDrain so
  POST /drain?off is reachable from shipped code
- HubEventLogStore/HubRunQueue: WAL journal mode + busy_timeout, and stamp
  sequences from lastInsertRowid instead of SELECT MAX(sequence)
- HubInstanceLock.acquire: degrade to an unheld lock when SQLite is
  unavailable instead of refusing hub startup; only BUSY/LOCKED still
  raises HubLockHeldError
- ensureHubWebSocketServer: retire an unusable discovered hub through the
  shared retireDiscoveredHub (busy hubs are attached to, drain precedes
  shutdown, discovery cleared only when the hub actually retired)
- replay adapter: advance the cursor past eventId-deduped events, cap
  replay pages, stop when the cursor stalls, and drop the dedupe set after
  the buffered flush so it cannot grow for the socket lifetime

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

* fix(hub): derive the singleton e2e challenger cwd portably

The challenger's working directory was derived by round-tripping the
discovery path through a file: URL and stripping the last pathname
segment. On Windows that yields a POSIX-style '/C:/...' path, which is
not a valid spawn cwd, so the spawn fails ENOENT before the singleton
lock is ever contested and the Windows SDK test job goes red.

The data dir is simply the discovery file's parent: use dirname().

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop stored capability lists from silently revoking tool calling for custom models (#13476)

* fix(core): seed tools capability when custom model capabilities are synthesized from boolean flags

For a models.json entry with no explicit capabilities list, toStoredModelInfo
synthesized a capability array purely from boolean convenience flags (e.g.
supportsReasoning: true -> ["reasoning"]). modelSupportsToolCalling fails open
only for a missing or empty list, so the synthesized non-empty list read as an
authoritative denial and silently stripped every tool definition from requests
to custom OpenAI-compatible models (#13463).

Seed "tools" whenever the list was not explicitly authored and the boolean
projections made it non-empty, preserving the fail-open contract. Explicitly
authored capability lists remain authoritative and can still disable tools.

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

* test(core): cover stale catalog capability overrides

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

* fix: treat stored capability lists as non-authoritative for tool calling

The hasExplicitCapabilities guard still let two producers of tool-less
lists through:

- The VS Code legacy-override migration (legacyModelInfoToOverrides)
  persists explicit partial lists like ["prompt-cache"] into models.json
  for custom OpenAI-compatible models, which then read as an authoritative
  "cannot call tools" and drop every tool - same symptom as #13463.
- Any hand- or UI-authored partial list on a non-catalog model.

Stored entries and user-authored provider metadata have no way to declare
"cannot call tools" (there is no supportsTools field, and every writer
that authors a full list includes "tools"), so seed "tools" into any
non-empty list for a language model. Only generated catalog capabilities
remain authoritative - a genuine no-tools catalog model stays that way -
and non-language models (e.g. image generation) never gain a tools claim.

Also make legacyModelInfoToOverrides write "tools" into the arrays it
fabricates, matching the providers.json migration, so models.json stops
being poisoned for older readers.

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

---------

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

* chore(vscode): prepare 4.1.13 release

* chore(sdk): release v0.0.78

* chore(cli): release v3.0.57

* fix(core): run hub e2e files serially so daemon timing budgets survive CI contention

singleton.e2e.test.ts (added in #13468) spawns real daemons and runs for
~15s. Vitest's default file parallelism let it run alongside
shutdown.e2e.test.ts, whose assertions are wall-clock bound: discovery
within 10s, exit within 5s, and a 2s shutdown watchdog. On the 2-core
windows-latest runner that contention alone broke those budgets, failing
the shutdown test two different ways across runs — once never observing
discovery, once with the daemon forced to exit before its HTTP 202
flushed (socket hang up). The test passed on Windows before #13468 and
has failed every SDK publish run since.

* chore(desktop): release v0.0.16

* test(sdk): give windows-sensitive suites realistic timeouts

Four consecutive SDK publish runs failed on windows-latest, each on a
different test, all of them plain timeouts: two @cline/shared SQLite
tests at the 5s vitest default, core's bash executor at 10s, and the hub
singleton endpoint test at 10s. The 2-core Windows runner spawns forks
and takes SQLite locks slowly enough to blow those budgets under load.

These timeouts guard against hangs; they are not timing assertions (the
one suite that does assert elapsed time, shutdown.e2e, was fixed by
removing file-level parallelism instead). Raise core to 20s and give
@cline/shared an explicit 15s in place of the inherited 5s default.

* fix(telemetry): emit task.completed from every session teardown path (#13489)

The task.completed fallback lived only inside shutdownSession, but
stopSession/dispose route interactive sessions with a terminal reported
status through releaseSessionRuntime, which never emitted. Truthful
session-status reporting (shipped in 4.1.11) re-routed a large share of
interactive stops onto that branch and silently dropped the event.

Route the emission through a single choke point,
emitTaskCompletedOnTeardown, called from both shutdownSession and
releaseSessionRuntime. The completion criterion no longer reads
session.status: interactive sessions use the recorded final-turn
outcome (lastInteractiveTurnFinishReason), non-interactive sessions
keep the existing input.status === "completed" logic. A new
taskCompletedEmitted flag (also set by the submit_and_exit observer)
enforces exactly one task.completed per session. failSession now
records the errored final turn so a stale "completed" from an earlier
turn can never leak into the teardown emission. Telemetry only; no
user-facing behavior changes.

* chore(vscode): release v4.1.14

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on (#13498)

* fix(vscode): honor MCP auto-approve settings for SDK tool calls

The SDK extension required both the global 'Use MCP servers' auto-approve
toggle AND each tool's per-tool autoApprove flag before silently approving
an MCP call, while the legacy extension treated them as either/or. Restore
the legacy OR semantics so toggling MCP auto-approve works again.

Also key toolPolicies by the registered SDK tool name (via
defaultMcpToolNameTransform, now exported from @cline/core) instead of raw
server__tool. Servers whose names contain sanitized characters (e.g.
marketplace names like github.com/user/repo) or exceed 64 chars produced
policy keys that never matched the registered tool, so those MCP tools ran
without any approval gate; the live auto-approve lookup now re-applies the
transform instead of string-splitting the name.

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

* Revert "fix(vscode): honor MCP auto-approve settings for SDK tool calls"

This reverts commit 86c568fbba.

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on

The SDK extension only auto-approved an MCP call when the global 'Use MCP
servers' auto-approve toggle AND that tool's per-tool autoApprove flag were
both set, so toggling MCP auto-approve appeared to do nothing and users had
to opt in each tool individually. The toggle alone now governs all MCP
tools; the per-tool flag is no longer consulted.

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

---------

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

* chore(vscode): release v4.1.15

* fix(cli): remove the $4.99 ClinePass promo copy (#13514)

The $4.99 first-month promo is ending, so the CLI's first-launch "Try ClinePass" dialog should no longer advertise it. Also drops the leftover CLI_PROMO_CODE plumbing, which has been an empty string since the promo-code flow was removed.

* fix(vscode): resolve hook workspace identity from the window, not shared global state (#13352)

* fix(vscode): resolve hook workspace identity from the window, not shared global state

Hook discovery, hook cwd selection, and the workspaceRoots metadata passed
to hook scripts all read the workspaceRoots/primaryRootIndex global state
keys. Global state lives in ~/.cline and is shared by every Cline instance
(all VS Code windows, the CLI, the JetBrains plugin), and nothing writes
these keys anymore, so hooks resolved against whatever project some other
or older instance last recorded. With a second window open on another
project, a workspace's .clinerules/hooks scripts were never discovered.

Resolve workspace roots via a single guarded helper backed by
HostProvider.workspace.getWorkspacePaths() (in-process, window-scoped,
same as refreshHooks): blank paths are filtered, a host-bridge failure
degrades to no workspace roots instead of silently disabling global hooks
or skipping blocking PreToolUse guards, and one resolution is threaded
through hooks-dir discovery, cache misses, cwd selection, and hook input
metadata so they can't disagree (previously up to four host lookups per
hook execution — real gRPC round trips in the standalone host). Roots and
hooks dirs are matched on whole path segments with the longest root
winning, so prefix-sharing or nested workspace roots resolve to the right
project. The adapter creates the runner once per event and skips no-op
runners, making creation the single resolution point; the separate
hasHook/getHookInfo checks are removed. The dead workspaceRoots and
primaryRootIndex state keys are dropped, and the four hand-rolled
HostProvider.workspace test stubs are consolidated into one shared
helper.

* test(vscode): add e2e coverage for workspace-scoped hook discovery

Boots real VS Code with the packaged extension against the workspace
fixture, sends a prompt, and asserts the fixture's UserPromptSubmit hook
was discovered from the open window's workspace, executed with that
workspace root as its cwd, and received the same root in its
workspaceRoots input — the end-to-end contract the hook workspace
identity fix establishes.

* test(vscode): isolate the e2e hook fixture from the shared workspace

The UserPromptSubmit fixture hook lived in the shared e2e workspace, so
every prompt-sending spec executed it (hooksEnabled defaults to true) —
and its cold PowerShell spawn on Windows pushed chat.test.ts past the
5s expect timeout. hooks.test.ts now overrides workspaceDir to a
dedicated workspace-hooks fixture, so only the hooks spec pays the hook
spawn.

* fix(hub): cap hub-events db size so it can't fill the disk (#13516)

* fix(hub): cap hub-events db size so it can't fill the disk

Row/time retention alone didn't bound disk usage: envelopes carrying
full session snapshots reach hundreds of KB each, so retained rows
could total tens of GB, sweeps only ran hourly, and DELETE never
shrinks a SQLite file. Enforce a 64 MiB size budget in prune() (oldest
rows first, VACUUM to return the space), and also prune after every
16 MiB appended so bursts can't outrun the hourly timer.

Fixes #13505

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

* fix(hub): tolerate VACUUM failure on a full disk

VACUUM needs scratch space and can fail in exactly the state a
ballooned event log causes. The byte-budget deletes already bound live
data, so swallow the error and let the next sweep retry the reclaim
instead of aborting startup pruning and disabling the durable log.

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

* fix(hub): count the size budget in UTF-8 bytes, not characters

envelopeJson.length (UTF-16 units) and SQLite LENGTH() (characters)
undercount multibyte text by up to 3x, which could leave a CJK-heavy
log settled above budget and re-running VACUUM every sweep. Use
Buffer.byteLength and LENGTH(CAST(... AS BLOB)) instead.

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

---------

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

* fix(sdk): carry root overrides into the Node smoke-test sandbox (#13517)

ci-node-smoke.ts installs the packed SDK tarballs with a plain npm
install in a fresh temp dir, where the repo root package.json overrides
do not apply. When @sap-cloud-sdk 4.9.0 shipped (2026-08-24) it broke
@sap-ai-sdk/ai-api 2.14.0 (via @jerome-benoit/sap-ai-provider in
@cline/llms) with ERR_PACKAGE_PATH_NOT_EXPORTED, failing the smoke step
on every PR even though the root already pins @sap-cloud-sdk/* to 4.6.0.

Copy the root overrides block into the generated sandbox package.json
so the smoke install resolves the same pinned versions as the repo and
future third-party releases cannot break it independently.

* chore(sdk): release v0.0.79

* fix(vscode): don't steal last-used provider from ClinePass on credential refresh (#13520)

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

* fix(hub): flush the /shutdown 202 before daemon teardown

The /shutdown handler queued teardown on a microtask, which runs before
the event loop's write phase, so the daemon could process.exit() before
the accepted 202 was handed to the socket. Unix masked it (uv_try_write
lands small loopback writes synchronously); Windows has no such fast
path and lost the race regularly — the recurring shutdown.e2e.test.ts
'socket hang up' failures on windows-latest. Start teardown from the
response's write callback instead, with an idempotent 1s fallback so a
client that vanishes mid-write cannot strand the daemon, and send
Connection: close so the client gets a FIN rather than an abort.

Since the flakiness this compensated for is fixed at the source, restore
maxWorkers: 2 for the Windows core suite (serializing it cost ~3 min of
CI per run), and raise the e2e daemon discovery hang guard 10s→30s —
it guards against hangs, not runner speed.

* chore(cli): release v3.0.58

* fix(core): prevent search_codebase from crashing the process on giant single-line files (#13525)

* fix(core): prevent search_codebase from crashing the process on giant single-line files

searchWithRipgrep buffered all of rg's --json stdout into one string. Each
JSON event embeds the full text of the matched line (--max-columns is
ignored in JSON mode), so searching a directory of serialized trace dumps
(single-line multi-hundred-MB JSON files) accumulated gigabytes of stdout
until string concatenation threw RangeError: Out of memory inside the
stream data handler. That throw is outside the tool's try/catch, so it
escalated to an uncaughtException and killed the CLI/hub daemon.

Parse rg's JSON events incrementally line by line, drop events larger
than 256KB, truncate matched/context lines to MAX_LINE_CHARS, and stop
reading once maxResults is reached. The fallback regex scan now skips
files larger than 10MB (reporting the skip count) and truncates its
context lines the same way.

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

* simplify search_codebase crash fix to a minimal diff

Replace the incremental JSON-event parser with three small guards: stop
buffering rg stdout past 10MB, drop the trailing partial event before
parsing, and slice fallback context lines to MAX_LINE_CHARS. Drops the
fallback file-size skip and skip-count reporting.

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

---------

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

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag (#13522)

* chore(vscode): remove per-tool MCP auto-approve checkboxes from webview

MCP auto-approval is now governed solely by the global 'Use MCP servers'
toggle; the SDK approval path (shared with the CLI and desktop app) has no
per-tool granularity, so the per-tool and 'Auto-approve all tools'
checkboxes were no-ops that implied control that no longer exists. Remove
them from the MCP settings view and chat tool rows. The autoApprove arrays
in cline_mcp_settings.json and the toggleToolAutoApprove RPC are left
intact for the legacy extension.

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

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag

Keep the checkbox components, handlers, and RPC plumbing intact but gate
rendering behind SHOW_MCP_PER_TOOL_AUTO_APPROVE=false: the SDK approval
path (shared with the CLI and desktop app) is all-or-nothing via the
global 'Use MCP servers' toggle, so the per-tool checkboxes were no-ops.
Flip the flag back on if the SDK gains per-tool approval granularity.

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

---------

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

* fix(tools): create new files with the platform-native line ending (#13521)

* fix(tools): use platform-native EOL for new files and preserve CRLF in apply_patch updates

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

* simplify to the minimal new-file EOL fix

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

* extract shared normalizeNewFileLineEndings helper

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

---------

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

* Add suggested schedule templates to the desktop Schedules page (#13529)

* Add suggested schedule templates to desktop Schedules page

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

* Fix unreadable selected text in inputs caused by selection utility conflict

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

* Restyle Suggested section label as small gray uppercase

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

* Hide suggested schedule cards that match an existing schedule name

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

---------

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

* Disable the agent todo tool and hide the Agenda UI in the desktop app (#13530)

* remove todo tool and Agenda UI, keep schedule-only tasks tool

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

* chore: biome formatting fixes

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

* restore agenda backend; disable todo kind behind a flag instead of deleting

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

* keep agenda automation pump idle while the todo tool is disabled

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

* remove todo tool and Agenda UI altogether (revert the disable-flag hybrid)

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

* restore all agenda code to main state

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

* disable agent todo tool and hide Agenda UI behind flags

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

---------

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

* Add Desktop App and Cloud Platform to bug report issue template (#13532)

* Add Desktop App and Cloud Platform to bug report surfaces

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

* Rename Surface Diagnostics field to Diagnostics

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

---------

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

* desktop: sidebar navigation cleanup with New/Schedule/Customize rows and dialog-based search (#13533)

* desktop: clean up sidebar navigation chrome

- Give New Task its own full-width labeled row below the logo row
  instead of an ambiguous icon next to the agenda toggle
- Wire the New Task row to the home action so starting a new task
  clearly takes you home (the logo still works as a fallback)
- Swap back/forward chevrons for browser-style arrow icons and
  bump their size

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

* desktop: sidebar New/Schedule/Customize rows and always-visible search

- Stack New (plus icon), Schedule, and Customize as full-width labeled
  rows below the logo; whole row highlights on hover via sidebarItem
- New starts a fresh task (home), Schedule opens Settings > Schedules,
  Customize opens the Customizations sections (Plugins first)
- Show the session search bar permanently above the sessions list
  instead of hiding it behind a search icon toggle

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

* desktop: move session search into a dialog behind a logo-row icon

- Replace the inline sidebar search bar with a search icon in the
  logo row that opens a cmdk command dialog listing sessions
- Selecting a result opens that session and closes the dialog
- Remove the agenda/tasks toggle the icon replaces, along with the
  now-unreachable sidebar Agenda panel (the welcome screen still
  surfaces agenda tasks)

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

* desktop: load full session history when the search dialog opens

Addresses Greptile review on #13533: the dialog only searched the
currently loaded history batch, so older unloaded sessions could not
be found. Opening search now kicks off loadAllSessions() (the hook's
purpose-built global-search loader), and the empty state reads
'Searching older sessions...' while more history is streaming in.

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

---------

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

* Hide Channels and Agents sections from desktop app sidebar (#13527)

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

* Desktop app: organize sidebar sessions into Pinned, Scheduled, and Tasks sections (#13528)

* Add Pinned/Scheduled/Tasks categories to desktop app sidebar

Replace the Schedules and Favorites filter-menu options with visible
collapsible category sections in the session sidebar, and rename the
Favorite action to Pin across the sidebar and sessions view.

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

* Grow full history window when Tasks show-more outpaces loaded tasks

loadMoreSessions treats its argument as a limit on all sessions, but the
Tasks show-more count only tracks Task rows, so once pinned/scheduled
rows pushed the loaded total past the requested count the call no-oped
and clicks went dead. Grow the whole history window via
loadOlderSessions instead, and only when the loaded tasks cannot fill
the next page. Addresses Greptile review on #13528.

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

* Auto-fill the Tasks page instead of fetching once per show-more click

A single 50-session window growth can consist entirely of pinned or
scheduled sessions, leaving a show-more click with no visible Tasks
progress. Replace the one-shot fetch with a page-fill effect that keeps
growing the history window until the requested Tasks page fills or
history runs out. Addresses the follow-up Greptile review on #13528.

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

* Halt page-fill retries after a failed history fetch

A failed fetch leaves the task count and has-more state unchanged,
which are exactly the conditions the page-fill effect fires on, so one
failing request would retry and re-toast forever. Halt the effect after
a failure and let the next explicit show-more click retry. Addresses
the third Greptile review on #13528.

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

---------

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

* Redesign desktop Model Providers page and split voice input into its own settings page (#13531)

* Redesign desktop Model Providers page and split voice input into its own settings page

- Group providers into Connected / Popular / All with auth-kind hints and
  connection status instead of per-row enable toggles
- Show browser sign-in (not an API key field) for OAuth providers, with a
  collapsed manual-key escape hatch where supported, plus explicit
  Connect / Disconnect / Sign out actions
- Move voice input to a dedicated Settings > Voice page that only offers
  connected transcription-capable providers, preselects a default model
  (streaming preferred), and stays disabled in the sidebar until a
  provider is connected

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

* Show native tooltip on the disabled Voice settings nav item

Disabled buttons drop pointer events, so the 'connect a model provider'
hint moves to a wrapping span for the browser tooltip to render.

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

* Drop letter avatars and gray provider ids from provider rows and voice chips

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

* Drop model counts from provider list rows

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

* Rename provider Connected status to Configured and drop the green styling

A settings entry is configuration, not a live connection; neutral gray
text avoids implying an active link, since the user still picks which
configured provider to use per chat.

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

* Resync provider catalog from disk when a settings save fails

Connect/disconnect/credential edits update the list optimistically; a
failed save now reloads the catalog instead of leaving the optimistic
state (and the view's module cache) claiming a configuration that was
never persisted.

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

* Rename oauthProvider test fixture to dodge CodeQL name heuristic

CodeQL's clear-text-storage query flags any identifier matching 'oauth'
as a credential source and traced the fixture's provider id into the
favorite-models localStorage write, which stores only provider/model id
strings. Renaming the fixture removes the false-positive source.

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

* Guard catalog reloads against races and resync detail drafts on failed saves

Optimistic provider mutations now bump a generation that discards any
in-flight catalog response, so a failed-save recovery reload can't
overwrite a newer edit with an older disk snapshot. The recovery also
remounts the provider detail panel via a reset token so its local field
drafts reflect the reloaded on-disk state instead of unpersisted edits
or an optimistically cleared disconnect.

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

* Fix failed-save recovery ordering and retry superseded reloads

Remount the provider detail only after the authoritative catalog reload
lands, so its drafts re-seed from disk state rather than the optimistic
values that failed to persist. When a concurrent edit supersedes the
recovery's in-flight response, retry the reload (bounded) instead of
dropping it, since that edit performs no reload of its own.

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

---------

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

* feat(desktop): Customize hub, sidebar overhaul, and settings polish (#13538)

* feat(desktop): merge customization pages into a Customize hub with inline marketplace

Replaces the Plugins page and the dedicated Marketplace page with a single
Customize hub. Tabs: Skills, MCP, Plugins, Rules, Hooks, Tools, each with
live counts. Tabs backed by a marketplace catalog render the installed
items followed by an inline browsable Browse section (CLI-hub style), so
installing from the catalog immediately reflects in Installed above.

- Installed cards restyled to mirror the browse-card anatomy: bg-card p-4
  containers, absolute top-right xs Uninstall matching Install, truncating
  semibold titles, primary-tinted icons, real Badge components instead of
  ad-hoc bordered spans, un-indented line-clamped descriptions
- Rules/Hooks/Tools rows brought into the same card language; redundant
  intro paragraphs (duplicating the page description) removed; Tools group
  headers match the Installed header style with counts
- Marketplace section header renamed to Browse; duplicate 'N results' row
  removed (the header count is the single source)
- MCP embedded view now shows the full marketplace instead of
  installed-only

* feat(desktop): overhaul sidebar sessions and navigation

Sessions list:
- Sort toggle removed; sessions are always grouped by project, with pinned
  sessions leading each group (both subsets ordered by recency). The
  Pinned/Scheduled/Tasks category sections and their time-mode paging
  machinery (page-fill effect included) are deleted
- Scheduled sessions get an inline clock icon next to the pin position;
  pin + clock render together when both apply, and the running/unread
  status dot now coexists with them
- One font size (text-sm) across the list: titles, timestamps, project
  headers, show-more buttons, empty states. sidebarText needed !text-sm
  because the default button size's text-base wins the twMerge conflict
- Gradient fade under the Sessions header once the list scrolls, so rows
  fade out instead of hard-clipping
- The session-detail hover card is controlled from the sidebar and closes
  on scroll (Radix receives no pointer events while scrolling, so it used
  to float over moving content)
- Sidebar min resize width raised 224->260px; the per-project show-more
  label truncates so its nowrap text can't force rows to overflow and clip
  timestamps at narrow widths

Navigation:
- Customize replaces the Plugins/Marketplace/Hooks/Rules/Tools sidebar
  entries; Schedules and Customize are hidden from the expanded settings
  nav (their top rows cover them) but stay reachable when collapsed
- The settings gear always opens General instead of resuming the last
  section; the Account no-op hover special case is gone
- The New row highlights (aria-current) while the fresh not-yet-started
  task page is showing and hands off to the session row once the task
  starts; hitting New also focuses the prompt input via a window-event
  signal (lib/prompt-input-focus.ts) since the sidebar and composer sit in
  distant subtrees
- Fixed the xs button size collapsing any icon-bearing button to 12x12
  (leftover has-[>svg]:size-3 from when xs was a micro button) — this was
  why Uninstall buttons rendered broken next to Install

* feat(desktop): polish settings pages and chat composer

Models page:
- The provider detail panel is always open: no X button, no empty
  no-selection state. It defaults to the first connected provider (falling
  back to the first in the catalog), which also removes the layout shift
  that happened when the page swapped between full-width and panel
  variants on selection
- Fixed the list pane becoming unscrollable while the panel was open:
  grid items default to min-size auto, so the pane grew past its track
  inside the overflow-hidden grid and its ScrollArea had nothing to
  scroll; wrapped it in a min-h-0 min-w-0 cell
- Add Provider opens a Dialog instead of swapping the page
  (AddProviderContent gained a dialog variant that renders only the form)
- Embedded inputs (provider search, model search, detail fields) share one
  EMBEDDED_INPUT_CLASS stripping the Input component's own border/dark bg
  tint/shadow/ring, which rendered as a mismatched inner box; the model
  search box uses the same h-9/px-3 frame as the provider search
- Model list flows with the page instead of a max-h capped inner scroller

Other pages:
- Account uses the shared PageFrame/PageHeader: left-aligned, text-3xl
  title, Sign Out in the header actions slot
- Desktop notifications is one General section: header row plus the
  Event/Notify/Sound matrix nested in a card, so its rows no longer read
  as top-level peers of Dark mode; 'Available in the desktop app' label
  removed
- Schedule page retitled from Schedules with a real description; Customize
  description rewritten

Chat composer:
- The voice dictation button only renders once a voice model is
  configured (Settings -> Voice); the unconfigured deep-link state is
  gone (prop type kept for an easy restore)

* chore(desktop): release v0.0.17

* fix(desktop): unblock sdk-test lint on the voice-input model picker (#13553)

The model picker renders a radiogroup of styled buttons with role=radio
and aria-checked; biome's useSemanticElements flags the role as an
error, which fails the sdk-test Quality Checks lint for every PR
touching sdk/ or apps/ paths. Suppress with a justification — switching
to input type=radio needs a restyle and belongs to the desktop settings
work.

* fix(vscode): include rich workspace metadata in system prompt (#13518)

* capture richer workspace information for vs code extension

* fix(shared): redact credentials from workspace remotes

* fix(shared): avoid regex backtracking in remote redaction

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>

* Hide task costs on vscode when ClinePass is selected (#13515)

* fix: stop showing cost estimates for subscription-billed providers (#13552)

* fix(vscode): stop showing cost estimates for subscription-billed providers

Providers whose usage is covered by a flat-rate subscription (ChatGPT
Plus/Pro via openai-codex, ClinePass) are marked with
metadata.usageCostDisplay = "subscription" in the SDK, and the CLI
already suppresses dollar figures for them. The VS Code host collapsed
that value into "show" before it reached the webview, so the task
header and model pricing rows rendered API-rate cost estimates that
users read as real charges on top of their subscription.

Pass all three usageCostDisplay values ("show" | "hide" |
"subscription") through the catalog listing and render cost only when
the value is "show", matching the CLI's shouldShowCliUsageCost
policy.

* feat(llms): mark Claude Code as a subscription-billed provider

Claude Code is typically authenticated with a Claude Pro/Max
subscription, but its models reuse Anthropic API pricing metadata, so
Cline rendered per-token prices and API-rate cost estimates for usage
that is covered by the subscription. Set usageCostDisplay =
"subscription" on the provider (picked up by the CLI and the VS Code
webview) and suppress the price rows in the Claude Code settings card.

The Claude Code CLI can also run on API-key billing, where a real cost
exists; the provider cannot distinguish the two, so we prefer showing
no number over a misleading one.

* fix(vscode): suppress cost display until provider listings load

While the ListProviders request is in flight (or after it fails), the
usage-cost hook had no listing to consult and fell back to "show",
flashing the API-rate estimate at subscription users on every chat-view
mount — the exact display the previous commit removes. Return
"unknown" whenever listings are absent; consumers already render cost
only for "show", so they suppress it during that window with no
changes. Briefly hiding a real cost is harmless, briefly showing a fake
charge is not.

* feat(desktop): customize macOS DMG install window (#13563)

* feat(desktop): add Retina DMG background tooling

* feat(desktop): customize the macOS DMG layout

* ci(desktop): validate DMG background assets

* fix(desktop): adjust DMG Applications icon position

* ci(desktop): drop redundant DMG artwork validation from publish workflow

Tauri's beforeBuildCommand already runs dmg:background (with its own
validation) at the start of the build/sign/notarize step, and the
release/beta config overlays do not override the build section, so this
step duplicated work the publish job performs anyway. PR-time coverage
lives in desktop-test.yml.

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>

* feat(desktop): sidebar time view, Customize/Marketplace split, and schedule page UX (#13570)

* feat(desktop): split Customize into Installed and Marketplace pages

The Customize hub previously embedded a Browse section inside every tab
that had a catalog. That inlining made each tab long and buried the
catalog. Customize is now the installed inventory only (skills, MCP,
plugins, rules, hooks, tools tabs pass marketplaceVariant="installed"
to the embedded MarketplaceView; McpServersContent grew the same prop),
with an outline Marketplace button in the header.

Browsing moved to a dedicated Marketplace settings section that renders
the previously dead "directory" variant of MarketplaceView: one list
across all catalog types with type-filter chips, wrapping tag chips,
and light rules separating the filter tiers from each other and from
the results. The Clear control now renders inline at the end of the tag
row only while a tag is active, the Updated date is gone, and the
header hosts an Installed button mirroring the one on the Customize
page. Directory subheader copy: "A curated set of plugins, MCP
servers, and skills from the Cline community."

Tag and type chips wrap to new lines instead of scrolling
horizontally.

* feat(desktop): sidebar time view with sections, sort toggle, and scheduled detection

Restores the time-sorted session list as the default sidebar view, with
collapsible Pinned / Scheduled / Tasks sections (headers appear only
once something is pinned or scheduled) and the page-fill effect that
grows the fetched history window until a Show-more click makes visible
progress. Project grouping stays as the alternate mode behind a
one-click sort toggle whose icon reflects the active mode — the old
dropdown cost an extra click for a two-option choice.

Scheduled sessions are detected two ways: the hub-schedule origin
trigger in session metadata, plus a fallback that asks the hub which
session ids belong to schedule executions (list_routine_schedules,
fetched on mount and every two minutes, merged into a rolling set).
The fallback matters because locally executed scheduled runs do not
reliably stamp the trigger into session metadata — a real scheduled
session created today carried only {mode:"user"} provenance. The
scheduled clock icon now leads the row, left of the title; pin and
timestamp stay on the right.

The initial visible page grows from 10 to 30 rows so a tall sidebar
fills instead of stranding a stub of rows over empty space (history
fetches already start at 50).

The expanded sidebar's Customize row now hosts indented Installed and
Marketplace sub-tabs while a customize section is open; the active
sub-tab carries the full selected background while the parent keeps a
subtler one so the two simultaneous highlights read differently.

Also fixes the hover-card flash on click (logo card and session-row
cards): Radix HoverCardContent sits on a DismissableLayer, so a click
on the trigger registers as a pointer-down outside the card and
dismisses it, and the trigger's focus event immediately reopens it.
onPointerDownOutside preventDefault suppresses the dismissal; cards
still close on pointer leave.

* feat(desktop): schedule page row, dialog, and details UX polish

Schedule cards are now click targets: clicking anywhere on a card
outside its controls opens the details dialog (guarded via
closest("button,...") since every inline control, including the Radix
switch, renders a button element), with Enter/Space keyboard support.
The redundant eye button is gone. The remaining edit / run / pause /
delete buttons grow from the 12px icon-sm size to 28px targets with
16px icons, sized consistently with the adjacent enable toggle — the
icons use explicit size-4 classes so the Button base svg rule cannot
shrink them back.

The new/edit dialog gains breathing room between field labels and their
inputs (space-y-2 per field wrapper).

The details dialog no longer scrolls as a whole when the schedule JSON
is long: the dialog is a flex column capped at 85vh, the JSON pre
shrinks to the remaining space (min-h-0) and scrolls internally, and
the Runs tab list scrolls inside the tab the same way.

* feat(desktop): scheduled sessions UX — unified details dialog, run-now handoff, hidden steering, stuck-thinking fix (#13573)

* feat(desktop): merge schedule details into one view and open run-now sessions

The schedule details dialog drops its Overview/Runs tabs: one scrollable
column with the meta grid, the configuration JSON (capped at max-h-64
with internal scroll so it cannot crowd out what follows), and a Runs
section beneath it showing the three most recent runs with a ghost
"Show all N runs" expander (collapsed again whenever a different
schedule's details open). The "Full configuration for this schedule"
subtext is gone; the dialog passes aria-describedby={undefined} so
Radix does not warn about the missing description.

Run now hands you into the session it starts. The trigger command
queues the run and returns before the runner attaches a session id, so
after the toast the handler polls the schedule overview once a second
for up to 15 seconds — which doubles as keeping the page's run status
fresh (refreshSchedules now returns the fetched overview to make that
single-stream) — until the triggered execution reports its session id,
then calls onOpenSession. Guarded so it never auto-navigates after the
user left the page.

* feat(desktop): hide runtime steering messages from transcripts

Scheduled/automation runs inject user-role steering messages each
iteration ("[SYSTEM] This run is not complete until you call
submit_and_exit...", plus a team-obligations variant). The chat view
rendered them as user bubbles, as if the person had typed them — in a
scheduled session the transcript was mostly [SYSTEM] noise.

They are machinery talking to the model, not something the person said
or needs to read, so the transcript now hides them entirely:
MessageBubble renders null for any [SYSTEM]-prefixed user message.
Grouping still treats them as working-row machinery via a single
isSystemSteeringMessage predicate — they collapse into the run's work
span, are never a turn boundary, can never be mistaken for a run's
answer, and never advance the run count even when metadata is missing —
so work-block folding and checkpoint/edit run numbering stay correct.
A finished scheduled session now reads as prompt, work summary, answer.

* fix(desktop): poll history while an attached session's event stream is dead

Opening a scheduled session while (or right after) it runs left the
view stuck on the thinking shimmer until the user switched away and
back. Root cause is in core: the hub daemon executes scheduled runs on
a private LocalRuntimeHost inside createLocalHubScheduleRuntimeHandlers,
while the hub server only projects live events from its own session
host — so session.attach succeeds but no assistant/tool/status events
ever flow. And since multiple hub daemons share cron.db, a run claimed
by a different daemon is invisible to this hub regardless. The proper
core rewiring is tracked as ENG-2474.

Client-side heal that covers every case: while an attached history
session reports a busy status and no chat_event chunk has arrived for
five seconds (and no assistant bubble is mid-stream), poll every three
seconds — re-read canonical history, merged through the same dedupe
path hydration uses, and the session record's status — so the
transcript and the thinking indicator settle in place. Locally driven
turns keep chunks flowing, so the quiet-window guard keeps the fallback
inert there.

* chore(desktop): format workspace selector components

Biome formatting drift that landed on main; picked up by a formatter
pass over components/views/chat.

* fix(desktop): keep stale-stream poll inert during locally driven turns

The fallback poll could fire between a local submit and the model's
first chunk (optimistic user bubble added, stream quiet past the
window, no assistant bubble yet). It then replaced the optimistic
bubble — raw prompt text — with its canonical history twin, which is
stored wrapped in a user_input envelope. The rekey handler that runs
when the stream starts looks for a trailing user bubble matching the
raw prompt, finds only the wrapped copy, and appends a second bubble:
duplicated messages in normal interactive chat.

The poll now stays inert while a local turn is in flight
(turnEpoch !== turnSettledEpoch, or outstanding optimistic user
messages), checked both before polling and again after the snapshot
returns. Hydration marks the turn settled — the mount defaults
(epoch 0, settled -1) otherwise read as an open turn and would keep
the fallback inert forever for the scheduled-session case it exists
for. Applying a polled snapshot also rebuilds the live tool routing
keys, same as hydration, so later tool events update canonical rows
in place instead of appending.

* fix(desktop): keep the working indicator alive for narrating scheduled runs

Watching a scheduled run live: the first tool row appeared, then the
thinking indicator vanished with nothing streaming, and the rest of
the run (final answer, submit_and_exit) only showed up seconds later
in one lump.

inferHydratedChatStatus treats a "running" session record whose
transcript ends on an assistant message as a session that died without
a status flip and reports "completed". That heuristic is right for
stale records, but scheduled/automation models narrate between tool
calls, so a polled snapshot can genuinely end on assistant text
mid-run — the completed flip hid the working indicator, folded the
run early, and disarmed the stale-stream poll (status left the busy
set), dead-ending live updates until an in-flight poll happened to
deliver the finished run.

The heuristic now only applies once the transcript has actually gone
quiet (newest message older than two minutes — comfortably past model
latency plus tool runs). A recently active transcript keeps the
record's "running" verdict, so the indicator stays up and polling
stays armed until the record itself settles.

* fix(desktop): stale-stream poll mirrors the session record instead of inferring

Replaces the previous fix for the vanishing working indicator (the
time-window guard added to inferHydratedChatStatus) with a version
that adds no inference at all: the heuristic is restored to exactly
its long-standing form, and the poll now maps the session record's
status verbatim (mapSessionRecordStatus).

The record is the right authority in the poll's context: the sessions
this fallback serves have a live host maintaining their record, and it
flips to a terminal status when the run ends. Transcript-shape
inference belongs only where it has always lived — hydrating sessions
whose records may be orphaned — and would misread a mid-run snapshot
ending on assistant narration as a finished session, hiding the
working indicator and disarming the poll.

* fix(desktop): address review findings on steering detection and run-now matching

Steering detection additionally requires the injected-message marker
(meta.userRunSpan === 0) beside the [SYSTEM] prefix, so a person's
genuine prompt that happens to start with "[SYSTEM]" stays visible
and turn-counted. The failure direction is deliberate: an unstamped
injected reminder would merely show as a user bubble, while the
content-only check could hide a real prompt.

Run-now only follows the execution id the trigger reply itself named;
the newest-execution-for-this-schedule fallback could open a previous
run's session when the trigger failed to enqueue one.

* fix(desktop): report a failed run-now instead of confirming a start

A trigger reply without an execution means no run was enqueued (the
schedule may have been disabled or deleted since the page loaded). The
handler previously toasted "Run started" regardless and then silently
skipped the session-open polling. It now shows a destructive
"Run not started" toast, refreshes the schedule list so the row
reflects reality, and skips the polling entirely.

* fix(desktop): don't block the main thread on quit while stopping the sidecar (#13566)

Quitting the mac app beach-balled for ~5-7s. The shutdown POST was
built from the ws transport URL (appending /shutdown lands inside the
query string), so the sidecar was never told to exit, and stop() then
polled the child for up to 7s on the main thread - on macOS inside
applicationWillTerminate - before SIGKILLing it.

stop() now sends SIGTERM and returns immediately. The sidecar handles
SIGTERM with the same bounded (5s) graceful shutdown as the /shutdown
endpoint and exits itself, finishing session persistence as an orphan.

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

* feat(desktop): hover trash button on sidebar session rows (#13582)

Each session row shows a trash icon on the right while hovered (or
when the button itself is focused), opening the same delete
confirmation dialog the row's context menu uses. The row is a button
and buttons cannot nest, so the trash is an absolutely positioned
sibling inside a group/row wrapper, overlaid where the timestamp sits:
row hover hides the timestamp, shows the trash, and moves the row's
hover background to the wrapper group so it holds while the pointer is
on the trash itself.

* fix(desktop): install marketplace plugins and MCP servers in-process instead of spawning a cline binary (#13585)

* fix: install marketplace plugins and MCP servers in-process instead of spawning a cline binary

The desktop app sidecar and cline-hub shelled out to 'cline plugin install'
and 'cline mcp install' for marketplace installs. Packaged GUI apps inherit
launchd's minimal PATH on macOS and most desktop users have no cline CLI
installed at all, so installs failed with a red
'Executable not found in $PATH: "cline"' error.

Install via @cline/core's installPlugin/installMcpServer in-process instead,
matching what the VS Code extension already does. Also fix
parseMcpInstallArgs in @cline/core to treat the marketplace catalog's '--'
separator as end-of-options; previously the separator itself became the
stdio command.

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

* refactor: drop test-injection plumbing from marketplace installers

Call @cline/core's installPlugin directly instead of threading an
installer option through the marketplace entry points; tests stub the
core module instead.

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

* revert: keep cline-hub marketplace installs CLI-backed

The hub dashboard is launched via 'cline dashboard', so a CLI is always
present and CLINE_WRAPPER_PATH resolves it; the PATH bug only affects
the desktop app, which does not ship a CLI.

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

---------

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

* chore(desktop): release v0.0.18

* chore(vscode): release v4.1.16

* chore(sdk): release v0.0.80

* chore(cli): release v3.0.59

* fix(hub): stop shipping full transcripts inside broadcast hub events (#13587)

* fix(hub): stop shipping full transcripts inside broadcast hub events

Every session.updated (and session.created/detached/run.started) event
embedded the session's ENTIRE message transcript via readCoreSessionSnapshot,
even though no consumer reads snapshot.messages off an event — clients fetch
messages with the session.messages command. For a multi-megabyte transcript
this turns every status flip into megabytes per subscriber, floods the
durable event log, and (until the send-queue backpressure fix lands) lets a
slow subscriber balloon the hub process by one full transcript copy per
event — reported as a 25GB cline process on a 16GB Mac.

Strip snapshot.messages centrally in HubServerTransport.publish() so every
current and future event publisher is covered, the event log stores slim
envelopes, and cursor replay stays byte-identical with live fan-out. All
other snapshot fields (status, usage, model, workspace, checkpoint) are kept,
and command replies are untouched.

* fix(hub): never capture the transcript into event/reply snapshots

Replaces the publish-boundary strip with the real fix: don't build
message-bearing snapshots in the first place. emitSessionSnapshot no longer
re-reads the entire transcript from disk on every status flip, and
readCoreSessionSnapshot no longer reads it for any event or reply — a
snapshot is a state notification (status, usage, model, workspace,
checkpoint); the transcript is fetched via the session.messages command.
Checkpoint-restore snapshots (session-versioning-service) are untouched:
restore replies carry messages in their own dedicated field.

* chore(desktop): release v0.0.19

* chore(sdk): release v0.0.81

* chore(cli): release v3.0.60

* fix(vscode): avoid render crash on malformed api_req payloads in combineApiRequests (#13560)

* fix(vscode): stop pinning DeepSeek model count in catalog smoke test (#13600)

* feat(ui): share agent welcome hero (#13567)

* feat(ui): share agent welcome hero

* test(ui): cover welcome hero pointer states

* refactor(ui): keep welcome hero API minimal

* test(ui): verify welcome hero package assets

* fix(ui): inline welcome hero masks

* fix(tools): preserve a file's own CRLF line endings across apply_patch updates (#13512)

* fix(desktop): keep the window title bar draggable across views (#13572)

* fix(desktop): keep window title bar persistent

* fix(desktop): reserve persistent title bar space

* fix(desktop): polish persistent title bar layout

* Sign Windows CLI binaries with Azure Trusted Signing; surface app-control launch errors (#13021)

* feat(cli): sign Windows binaries with Azure Trusted Signing and surface app-control launch errors

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

* fix(cli): use _CLI-suffixed signing profile secret, normalize endpoint, fail loud on partial config

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

---------

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

* Tunnel ProtoBus over the existing Host Bridge (#13218)

* feat(core): tunnel ProtoBus over Host Bridge

* fix(core): harden Host Bridge stream lifecycle

* fix(core): serialize concurrent chunked responses per request

Streaming handlers deliver updates fire-and-forget, so two logical
responses for one request_id can be in flight at once. Chunked payloads
made forwarding non-atomic: each chunk write is an await, so concurrent
forwards could interleave their chunk sequences and the receiver --
which reassembles purely by arrival order -- would splice two payloads
into one. Route all forwards for a request through one promise chain; a
failed write rejects every later forward so a torn payload is never
followed by more chunks.

Rename the lock manager's instanceAddress to instanceOwner: it holds an
opaque per-spawn instance ID on the token path and a listener address
only on the CLI-harness path. Delete the caller-less getInstanceByPort
query that interpreted the owner as an address.

Also: document message_json as a legal wire encoding for small
payloads, close the gRPC client when startup fails, note the
intentional discard of the cancellation confirmation, and add the
proto's trailing newline.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* Build and Authenticode-sign a Windows x64 desktop installer in desktop releases (#13607)

* feat(desktop): build and Authenticode-sign a Windows x64 NSIS installer in desktop releases

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

* fix(desktop): pin OIDC-adjacent actions to commit SHAs in the Windows signing job

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

* fix(desktop): pin checkout and upload-artifact to commit SHAs in the Windows signing job

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

---------

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

* fix(desktop): show agent-created schedules on the Schedules page (#13613)

* fix(desktop): show agent-created schedules on the Schedules page

Schedule hub commands are scoped to the workspace registered by the
connection, but the desktop app's hub client registers the app launch
directory while agent-created schedules live under each chat's own
workspace folder - so they never appeared on the Schedules page.

Grant token-authenticated hub connections (which can already bind any
workspace at registration) explicit cross-workspace schedule access via
an allWorkspaces payload flag, and have the desktop sidecar request it
for routine schedule commands. Workspace-bound clients (local browser
origins) and default CLI behavior stay scoped.

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

* refactor(core): strip allWorkspaces flag from schedule inputs and pin it in the sidecar payload

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

---------

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

* Make suggested routine template prompts prescriptive about their final output (#13611)

* Make bug hunter routine template prescriptive about its final report

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

* Make remaining routine templates prescriptive about their final output

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

---------

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

* Desktop: surface scheduled-task final output — auto-expand submit_and_exit and render its summary as markdown (#13612)

* desktop: auto-expand submit_and_exit and render its summary as markdown

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

* desktop: render submit summary in full foreground color

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

* desktop: label the submit row 'Scheduled task completed'

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

* desktop: label errored submit_and_exit rows as failed

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

---------

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

* Add tooltips explaining Live and After recording badges on voice input models (#13610)

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

* Remove box shadow from chat message actions row (#13630)

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

* fix(desktop): make the Tauri shell work on Windows (#13632)

- Defer updater installation to the user-initiated restart on Windows:
  install() launches the NSIS installer and exits the process immediately,
  so the background cycle now downloads only and stages the bytes, and
  restart_to_apply_update installs them after stopping the sidecar.
- Spawn child processes (sidecar, git, cmd /C start) with CREATE_NO_WINDOW
  so the GUI-subsystem app doesn't pop visible console windows.
- Fall back to USERPROFILE when HOME is unset resolving the MCP settings
  path, matching the sidecar's homedir().
- Reap the sidecar after the Windows hard-kill so its exe file lock is
  released before the NSIS installer replaces it.

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

* fix(core): stop watching agenda spec dirs while the todo tool is disabled (#13629)

* fix(core): stop watching agenda spec dirs while the todo tool is disabled

Since #13530 disabled the agent todo tool, the Agenda UI, and the
automation pump, the hub still created fs.watch watchers on the global
agenda specs dir and on every workspace root recorded in the task store
(at startup and on scope access). Nothing consumes the watcher-driven
task events while the feature is off, and the task.* hub commands
already reconcile spec files on demand, so the watchers are pure
overhead - one OS watch handle per known workspace.

Wire watchFiles to AGENDA_TODO_TOOL_ENABLED the same way
automationEnabled is, preserving a host's explicit watchFiles opt-out
for when the flag is turned back on. Schedules are unaffected: the
schedule list has no file watcher and updates through hub commands and
published schedule events.

* fix(core): reconcile external spec edits inside updateTask

With the spec watchers off there is no background reconciliation, so a
task spec edited directly on disk made every same-store task.update fail
the signature check with "task spec changed outside the manager" until
an unrelated task.get or task.list happened to reconcile the scope.

Reconcile the task's scope at the start of updateTask (mirroring what
refreshAndVerifyTaskIntent already does for approve/run), skipping it
when the file reconciler itself is the caller to avoid recursing from
reconcileFileStore. An external edit now surfaces as the store's normal
stale-revision conflict, and a re-read-and-retry succeeds. This also
closes the pre-existing watcher debounce race for updates.

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently (#13565)

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently

Port the cline-provider refresh semantics to openai-codex and oca:
a transient refresh failure (network error, timeout, server 5xx) with an
already-expired access token now rethrows instead of returning null.
A null return means the refresh token was REJECTED and re-auth is
required; treating an outage blip as a rejection is what turned it into
a forced 'openai-codex requires re-authentication.' task stop while the
settings UI still showed the user as signed in.

Both providers also emit user.auth_refresh_soft_failure telemetry on
transient failures (the 'prevented logout' counter the cline provider
already has) and attach status/errorCode details to the genuine
invalid_grant logout event.

* refactor: collapse duplicate soft-failure telemetry branches and test

Review feedback: compute tokenExpired once and emit the soft-failure
event once in both providers, then return current credentials or
rethrow. Fold the codex soft-failure telemetry assertions into the
existing still-usable-token test instead of a near-duplicate case.

* fix: make OpenAI Codex (ChatGPT subscription) sign-in fail loudly instead of silently dead-ending (#13537)

* fix: make OpenAI Codex sign-in fail loudly instead of silently dead-ending

When callback port 1455 is already in use (e.g. by the Codex CLI or a
previous pending sign-in), startLocalOAuthServer returns a no-op server
and loginOpenAICodex would open the browser anyway, then dead-end:
the callback could never be received, and in the VS Code extension the
user just saw nothing happen after clicking 'Sign in to OpenAI Codex'.

- loginOpenAICodex now fails fast with an actionable 'port in use'
  error before opening the browser, unless the host provides manual
  code entry (the CLI's paste fallback keeps working)
- surface OAuth redirect errors (e.g. access_denied) instead of
  collapsing them into 'Missing authorization code'
- the extension dedupes concurrent sign-in clicks: a re-click re-opens
  the auth page of the pending flow instead of spawning a second flow
  that would collide with our own callback server
- browser-open failures now show an error message with the URL to
  open manually instead of only logging
- abandoned-flow timeouts no longer surface a confusing 'Missing
  authorization code' toast

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

* refactor: drop host-side codex login dedupe, keep flow identical to CLI

The SDK owns the failure handling now (fail-fast on an unbindable
callback port), so the extension keeps the exact same simple
loginOpenAICodex call the CLI uses. A second click while a flow is
pending gets the SDK's clear port-in-use error, same as running
'cline auth openai-codex' twice would. Keep only the CLI-parallel
onOpenUrlError surfacing (the CLI prints 'open the URL above
manually'; the extension's equivalent is an error toast with the
URL).

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

* test(e2e): cover Codex sign-in callback-port failure and redirect errors

Two driven-VS Code tests for the OpenAI Codex (ChatGPT subscription)
sign-in flow:

- with port 1455 occupied on both loopback families, clicking the
  sign-in button surfaces the fail-fast port-in-use toast
- with the port free, the callback server binds and an OAuth redirect
  error (access_denied) propagates to a visible error toast

The second test opens a real browser tab to the OpenAI auth page as a
side effect of the genuine sign-in click.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* feat(core): anchor agent-created schedules in the user's .cline schedules home (#13634)

* feat(core): anchor agent-created schedules in the user's .cline schedules home

Agent-created schedules inherited whichever workspace folder the chat
session happened to run in, scattering user-level routines across chat
and project folders. They were invisible to workspace-scoped listings
elsewhere, tied to folders that may be cleaned up, and each chat's
tasks tool saw a different set when checking for duplicates.

Anchor them in ~/.cline/schedules instead: the hub's scheduled-task
session defaults now resolve to that home (created on demand), so
agent-created schedules live and run in one stable user-level scope.
The tasks tool guidance now tells agents that scheduled sessions run in
the schedules home, so prompts must carry absolute paths to any project
they operate on.

Schedules created explicitly with a workspace (CLI --workspace, desktop
routine wizard) are unchanged, and existing rows keep their current
workspaceRoot - they stay visible through the all-workspaces listing
paths (#13613, #13633).

* test(core): restore any pre-existing CLINE_DIR after the agenda hub test

The test's cleanup deleted CLINE_DIR outright, so an environment that
had it configured would leave later tests in the same worker on the
default storage directory. Save the previous value and restore it.

* test(core): restore CLINE_DIR even when hub test setup throws early

Restoring the override in the try/finally missed failures thrown during
transport construction or start(), before the try was entered. Register
the restore with onTestFinished instead, which runs regardless of where
the test fails.

* fix(desktop): don't show providers as configured without real credentials (#13608)

* fix(desktop): don't show providers as configured without real credentials

The desktop settings marked any provider with a persisted settings entry
as Configured, but legacy VS Code migration and empty saves can seed
entries (e.g. qwen-code, sapaicore) holding only a default model and no
credentials. Move the CLI's isProviderSettingsUsable readiness check into
@cline/core, expose it as a computed 'configured' flag on the provider
catalog, and use it in the desktop's isProviderConnected.

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

* fix(desktop): resync catalog after saves so Configured badge updates live

Optimistic provider mutations can't know the sidecar-computed 'configured'
flag, so after connecting a keyless provider or saving cloud credentials
(e.g. a Vertex project id) the row stayed 'Not configured' until remount.
Silently refetch the catalog after each successful save, guarded by the
existing generation counter so newer edits discard stale responses.

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

* fix(desktop): claim a generation in post-save resync so overlapping refreshes can't apply stale snapshots

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

* fix(desktop): bump catalog generation on OAuth login success

Every other optimistic provider mutation claims a new generation; the
OAuth success path didn't, so a catalog load or resync still in flight
could arrive late and overwrite the just-connected state.

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

* fix(desktop): resync catalog after OAuth login instead of bare generation bump

The resync claims a new generation (discarding any stale in-flight
response) and its own fetch covers both the new OAuth connection and any
provider saved moments earlier, matching the post-save path.

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

---------

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

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint (#13626)

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint

Restoring a checkpoint runs git reset --hard, which moves the current
branch pointer. If commits were made after the checkpoint (by the user
or by the agent), the reset silently knocked them off the branch,
leaving them reachable only through the reflog.

Guard the reset: if HEAD no longer matches the commit the checkpoint
was created on, throw a descriptive error (including how many commits
would be dropped) instead of destroying history. Chat-only restore is
unaffected, and users who really want to discard the commits can reset
the branch manually first.

Fixes #13550

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

* fix(core): close the guard-to-reset race with an atomic ref update

The moved-HEAD guard read HEAD, ran further git commands, then reset
unconditionally, so a commit landing in that window could still be
knocked off the branch. Replace the reset's branch move with git's
native compare-and-swap (git update-ref HEAD <new> <old>), which fails
if HEAD no longer points at the verified commit, and follow with a bare
reset --hard to sync the index and worktree to the already-moved HEAD.

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

---------

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

* fix: hide history cost estimates for subscription-billed tasks (#13562)

* fix(vscode): hide history cost estimates for subscription-billed tasks

The task-header fix for subscription providers cannot reach history:
history rows render the stored totalCost (an API-rate estimate) and do
not know which provider ran the task, so the history page printed
$X.XXXX on every row and the recent-task chips in an empty chat view
rendered a $ chip even for subscription-billed tasks.

The SDK session records already persist the provider — the CLI's
history view uses it for exactly this — but the VS Code mappers dropped
it. Map it through both transports (HistoryItem.apiProvider for the
state-pushed taskHistory, TaskItem.api_provider for getTaskHistory) and
suppress the dollar figure per row when that provider's
usageCostDisplay is not "show", via a new useUsageCostVisibility
predicate shared by both surfaces.

Rows without a recorded provider (tasks predating the field, legacy
imports) keep showing the stored value — there is nothing to key
suppression on.

* test(vscode): e2e-verify history cost suppression in real VS Code

Seeds SDK session records (one openai-codex subscription task, one
anthropic usage-billed task) into the isolated CLINE_DIR before the
webview loads, then asserts in a real VS Code instance that both the
recent-task chips and the full history page render the dollar figure
only for the usage-billed task. Covers the two boundaries the unit
tests stub: on-disk records reaching getTaskHistory with provider
populated, and the provider listings delivering the subscription mark
to the webview.

* Fix scheduled tasks disappearing after desktop app updates (#13627)

* Fix hub-managed schedules being wiped by cron reconciliation on hub restart

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

* Require the virtual hub/schedules path when exempting specs from removal reconciliation

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

* Treat recorded source mtime as proof a spec is file-backed, closing the hub/schedules spoof gap

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

---------

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

* fix(shared): discover global rules at ~/Cline/Rules (#13614)

The VS Code Rules tab resolves the Documents folder via
'xdg-user-dir DOCUMENTS', which prints bare $HOME when no user-dirs
config exists (WSL/headless installs), so it reads and writes global
rules at ~/Cline/Rules. The SDK's rule search paths only covered
~/Documents/Cline/Rules, so those rules never reached the system prompt.
Add the missing path to the search list.

Fixes #13542

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

* feat: add searchable session history (#13420)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

---------

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

* Fix CLI crash when a remote MCP server is offline but enabled (#13639)

Remote (SSE/streamable HTTP) MCP connects run on the session.create
critical path, which the hub caps at 30s. Without a connect budget an
unreachable server spent the full 60s default request timeout (with the
SSE transport stuck in a reconnect loop), stalling session.create past
the hub deadline and tearing the whole session down - the interactive
TUI exited and one-shot runs failed. Stdio servers already have a
bounded initialize budget for exactly this reason; give URL clients the
same treatment with a 10s default connect budget that an explicit
timeout overrides in either direction.

Fixes #13597

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

* test(vscode): prevent E2E worker teardown hangs (#13644)

* test(vscode): capture external URLs in E2E runs

* docs(test): clarify browser capture rationale

* Add a GitHub integration step to the onboarding (#13225)

* Add feature flags to the app

* React to account updates

* Address comments

* Add a GitHub integration step to the onboarding

* validate domain and fix errors on auth

* Hide the step behind a feature flag

* update version

---------

Co-authored-by: John Choi <john.choi@cline.bot>

* fix(ci): stop e2e worker teardown timeouts and deflake hub daemon e2e on Windows (#13646)

* fix(e2e): stop VS Code e2e worker teardown from timing out

The ext-vscode-test-e2e job has been failing on main with 'Worker teardown
timeout of 60000ms exceeded' even though every test passes. Playwright only
reports an Electron app as closed once the process exits AND every holder of
its stdio pipes is gone (ChildProcess 'close' waits on the extra fd3/fd4
pipes Playwright creates for Electron). Any VS Code descendant that outlives
the main process (chrome_crashpad_handler, GLib's 'dconf watch' helper,
xdg-open browser handlers, VS Code 1.135's agent host CLI subprocess that
logs 'unable to kill the process') keeps those pipes open, so app.close()
never resolves and the worker teardown hangs on it until its 60s timeout
fails the job.

Harness fixes, each removing one source of that wedge:

- closeAppForTeardown now SIGKILLs the whole process group (taskkill /T on
  Windows) when app.close() times out, instead of only the main pid — and
  does so even when the main process already exited, which is exactly the
  wedged state. Playwright launches Electron detached, so pid == pgid.
- Launch VS Code with --disable-crash-reporter so no crashpad handler
  outlives the app holding the harness pipes.
- Seed the fresh user-data-dir with chat.disableAIFeatures: true so VS
  Code's own AI features (rolled out via server-side experiments, so CI
  breaks without any repo change) never start their agent host process.
- Drop the page.close() teardown: closing VS Code's last window quits the
  whole app, and ElectronApplication.close() on an already-exited app
  deadlocks; the app fixture's app.close() closes windows itself while the
  app is alive.
- Codex sign-in no longer opens a real external browser under E2E_TEST; the
  codex-oauth test drives the OAuth callback itself, and the browser was an
  orphaned process holding the harness pipes on the runner.

* fix(core): deflake hub daemon e2e tests on Windows runners

sdk-test on windows-latest fails intermittently in the hub daemon e2e
files:

- shutdown.e2e.test.ts dies with a bare 'Error: socket hang up'. That
  message is the ws handshake (http.ClientRequest) failing, not the
  /shutdown fetch (an undici failure prints 'TypeError: fetch failed'):
  a freshly spawned bun daemon on a loaded 2-core Windows runner
  occasionally drops its first accepted connection before writing the
  upgrade response. Real hub clients reconnect with backoff, and the test
  asserts shutdown behavior rather than first-connection reliability, so
  openAuthenticatedSocket now retries transient handshake failures within
  a 15s budget.
- singleton.e2e.test.ts times out waiting for daemon discovery: it still
  used the 10s hang guard that 0cfc90158 already raised to 30s in
  shutdown.e2e.test.ts for the same reason. Use the same 30s guard.
- Raise the e2e testTimeout to 60s so a test that legitimately spawns two
  daemons back to back can survive slow-runner startups instead of the
  discovery hang guard being cut off by the test timeout.

* feat(desktop): render tool output images as attachments (#13643)

* fix(desktop): render tool output images as attachments

Add support for displaying media returned by tool calls (e.g. screenshots)
as rendered images with expand-to-fullscreen capability instead of raw
base64 text. Introduces an `ImageCarousel` component for navigating
multiple images, propagates the expand handler to tool message blocks,
and extracts/validates output media in tool summaries.

* test: cover multi-image and canonical media extraction in tool output (#13645)

extractOutputMedia and the desktop tool-message rendering path were only
ever exercised with exactly one distinct valid image, and
canonicalInlineMedia (MCP-style type: "media" blocks for audio/video/file)
had zero coverage. Add tests for: multiple distinct images in one tool
result (parser + desktop carousel navigation), inline audio via the
mime_type key spelling, canonical video/file media blocks, and rejection
of an invalid canonical image block.

---------

Co-authored-by: Harrison <harrison@cline.bot>

* chore(desktop): release v0.0.20

* feat(sdk): add discovery boundary ahead of Agent Plugins support (#13017)

* ENG-2490: Propagate session aborts to teammates (#13647)

* fix(core): propagate session abort to teammates

* fix(core): persist aborted teammate tasks as cancelled

* fix(core): settle teammate work on session abort

* fix(core): isolate replacement runs from stale aborts

* refactor(core): narrow teammate task status metadata

---------

Co-authored-by: abeatrix <beatrix@cline.bot>

* fix(llms): use AI SDK 7 Langfuse telemetry (#13651)

* fix(llms): use AI SDK 7 Langfuse telemetry

* test(llms): cover Langfuse runtime context

* chore(llms): built-in model list update 1787907289186 (#13663)

* chore(llms): built-in model list update 1787907289186

Result of `bun run build:models`.
Includes updated model list and fixed formatting issues across codebase.

* test(llms): update GLM reasoning toggle expectation

* test: cover session search fallback on hub timeout and rejection (#13642)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

* test: cover sidecar search fallback on hub timeout and rejection

The existing search_sessions tests only exercised the index-hit and
empty-index-fallback paths with an immediately-resolved hub reply.
Add coverage for the two other realistic Hub-connection failure
modes the fallback is meant to tolerate: the hub call rejecting, and
the hub call hanging past the 750ms withSearchDeadline race.

---------

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

* fix(core): refresh Cline models from live catalog (#13670)

* feat(ui): share attachment drop zone (#13672)

* feat(ui): share attachment drop zone

* fix(ui): cancel disabled attachment drops

* chore(ui): simplify drop zone surface

* chore(ui): release v0.2.0-next.8

* Chore/bump undici mermaid (#13675)

* chore(deps): bump mermaid to 11.16.1 and raise undici floor to 7.29.0

* chore(deps): patch js-yaml and body-parser in the npm-managed subprojects

* fix(llms): make Langfuse tracer detection survive minified release builds (#13680)

* fix(llms): recognize direct tracer providers

* fix(llms): make Langfuse tracer detection survive minified release builds

Release binaries are compiled with minify enabled, which renames classes,
so initializeLangfuseTelemetry's constructor-name guard never matched
"ProxyTracerProvider" and silently returned readiness=false in every
production build (hub log: "creating span processor" followed by
"initialized readiness=false" with no branch message in between). Dev runs
execute unminified source, which is why the same env vars worked there.

Replace every constructor-name comparison with checks that survive
minification: detect the proxy structurally via getDelegate, distinguish a
recording provider from the no-op fallback by its lifecycle methods, and
confirm our NodeTracerProvider registration by object identity. When a
foreign provider already owns the global slot, attach the Langfuse span
processor to it when it accepts processors, and otherwise shut down the
orphaned provider and report the rejection instead of bailing silently.

Verified by bundling the module with Bun minify:true against the real
OpenTelemetry packages: the previous code reproduces readiness=false
(provider class name mangles to "H2"), the new code initializes with
readiness=true.

* fix(vscode): prevent hook spawn failures from crashing the core process (#13422)

* fix(vscode): prevent hook spawn failures from crashing the core process

A hook child-process spawn failure emitted "error" on HookProcess with no
listener registered, which Node's EventEmitter turns into an uncaught
exception - killing the entire cline-core process instead of failing the
one hook open. Guard the emit behind listenerCount so the rejection (which
StdioHookRunner handles) is the only propagation path.

The trigger was a workspace root that no longer exists on disk passed as
the spawn cwd: Node reports a nonexistent cwd as a misleading ENOENT on
the launcher binary ("spawn /bin/sh ENOENT"). Validate cwd existence in
HookProcess right before spawning - falling back to no explicit cwd with
a warning that names the missing directory - and when a spawn still fails
ENOENT because the directory vanished in between, name it in the error
message instead of blaming the shell.

* fix(vscode): fail hooks with a missing working directory instead of relocating them

Running a hook whose assigned cwd no longer exists from the host
process's own working directory would let its relative paths read and
write an unrelated location (e.g. the IDE install directory). Reject
before spawning, with an error naming the missing directory; the runner
reports the hook as failed and the task continues. Also carry pre-spawn
failure messages into HookExecutionError details so the cause is not
reduced to a bare "exited with code 1".

* fix(vscode): thread task id into hook runner creation so execution telemetry fires (#13547)

The SDK hooks adapter created every hook runner without a task id, and
StdioHookRunner gates all captureHookExecution calls on one being set —
so the next variant emitted zero hooks.execution events while discovery
telemetry fired normally. Pass the task id (and tool name for the tool
hooks) at all five factory.create call sites, and pin the threading
with a regression test.

* Desktop marketplace redesign: two-pane explorer with full catalog metadata (#13653)

* feat(desktop): add marketplace design exploration prototypes (storefront, explorer, registry)

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

* fix(desktop): render catalog icon tiles without percentage padding

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

* feat(desktop): drop placeholder icon tiles from explorer marketplace direction

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

* feat(desktop): make explorer the marketplace view, drop design exploration harness

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

* feat(desktop): add category tag filters to marketplace explorer

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

* feat(desktop): collapse marketplace category pills behind a more toggle

* feat(desktop): remove maturity badges and CLI install section from marketplace

---------

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

* fix(core): propagate parent aborts to delegated subagents (#13677)

* fix(core): propagate parent aborts to delegated subagents

* docs(core): narrow delegated abort guarantees

* fix(core): release delegated sessions after execution

* fix(core): scope abort listeners to active runs

* fix(core): inherit parent runtime pid for subagents

* fix(desktop): keep Stop available for running child agents (#13678)

* fix(desktop): keep Stop available for running child agents

* fix(desktop): reconcile aborted tool activity

* fix(desktop): guard abort and agent polling races

* fix(desktop): preserve authoritative abort status

* fix(desktop): track queue-verified completion

* test(desktop): trim duplicate abort coverage

* fix(desktop): settle delayed queue verification

* fix: sanitize stored API keys and make provider credential rejections actionable (#13549)

* fix(vscode): sanitize pasted provider API keys at the settings write boundary

Clipboards smuggle control and invisible formatting characters (newlines,
zero-width spaces, BOM) into pasted API keys. The masked key field hides
the corruption and providers reject the key with a 401 indistinguishable
from a genuinely wrong key. Strip those characters and surrounding
whitespace once in the provider config store write path, so both backing
stores (legacy state secrets and providers.json) receive the clean value.
A whitespace-only value now clears the key.

* feat(llms,vscode): classify provider 401/403 as auth errors and surface actionable guidance

Add an "auth" ProviderErrorClass, assigned when the HTTP layer reports
401/403 — status-only on purpose, since provider bodies can quote words
like "unauthorized" without the request being an auth failure. The class
rides the existing errorClass plumbing (finish -> run-failed ->
AgentErrorEvent), so every host receives it with no new wiring.

In the VS Code chat surface, rewrite classified credential rejections
from BYOK providers into actionable text pointing at the API key
configuration, keeping the provider's raw body as a diagnostic tail.
Raw bodies alone are dead ends: Mistral, for example, answers an
identical {"detail":"Invalid API Key"} for a wrong, empty, or
wrong-scope key. Cline-account providers keep the JSON path so the
webview still renders their auth failures as a sign-in card.

* Fix ask-question option text not wrapping (#13718)

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

* chore(desktop): release v0.0.21

* fix(core): stop an empty capability list from stripping image input (#13583)

`modelHasCapability` documents a missing or empty capability list as
carrying no signal, so each gate declares its own default. Two readers
bypassed it and read `capabilities` directly, where an empty list is not
nullish but `[].includes(x)` is false:

- the session runtime's `modelSupportsImages` metadata used
  `capabilities?.includes("images") ?? true`, so the intended fail-open
  never fired for an empty list and the file-read tool silently dropped
  every image from the request;
- `toProviderModel` projected an empty list onto `false`, telling pickers
  a model definitively lacks vision, attachments, and reasoning when
  nothing had been declared.

Both now route through the shared helpers, which state their unspecified
default explicitly: `modelSupportsImageInput` fails open for a capability
gate, and `declaredCapability` preserves `undefined` for `ProviderModel`'s
tri-state booleans. A populated list stays authoritative in both.

A thinking config now short-circuits `supportsReasoning` instead of being
OR-ed with the capability read, so its absence no longer collapses the
tri-state to `false`.

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* fix(llms): translate gateway capabilities in one place (#13584)

* fix(core): stop an empty capability list from stripping image input

`modelHasCapability` documents a missing or empty capability list as
carrying no signal, so each gate declares its own default. Two readers
bypassed it and read `capabilities` directly, where an empty list is not
nullish but `[].includes(x)` is false:

- the session runtime's `modelSupportsImages` metadata used
  `capabilities?.includes("images") ?? true`, so the intended fail-open
  never fired for an empty list and the file-read tool silently dropped
  every image from the request;
- `toProviderModel` projected an empty list onto `false`, telling pickers
  a model definitively lacks vision, attachments, and reasoning when
  nothing had been declared.

Both now route through the shared helpers, which state their unspecified
default explicitly: `modelSupportsImageInput` fails open for a capability
gate, and `declaredCapability` preserves `undefined` for `ProviderModel`'s
tri-state booleans. A populated list stays authoritative in both.

A thinking config now short-circuits `supportsReasoning` instead of being
OR-ed with the capability read, so its absence no longer collapses the
tri-state to `false`.

* fix(llms): translate gateway capabilities in one place

Three producers built gateway model definitions from catalog `ModelInfo`,
and each carried its own hand-written `switch` over the capability list.
Nothing tied them together, so they drifted:

- builtin providers always emitted a capability list, so a model whose
  catalog entry declares no capabilities became `["text"]` where the other
  producers emitted `undefined`. `modelSupportsToolCalling` fails open only
  for an absent or empty list, so that list read as an authoritative denial
  and stripped every tool definition from requests to the affected language
  models (dify, sapaicore, opencode, and the Codex CLI);
- the OpenAI-compatible path mapped an `audio` capability that
  `ModelCapabilitySchema` does not define, while the other two dropped it;
- the pass-through capabilities (`streaming`, `files`, `temperature`, ...)
  were enumerated explicitly in one, folded into `default:` in another,
  and ignored in the third.

One exported `toGatewayModelCapabilities` now serves every producer. It is
built on a `Record<ModelCapability, GatewayModelCapability | null>` rather
than a `switch`, so extending `ModelCapabilitySchema` without deciding the
new capability's mapping fails to compile instead of silently falling
through to a default.

The conformance tests walk the capability state space taken from
`ModelCapabilitySchema` itself and assert the real producers agree with the
translator, so a future producer that maps capabilities on its own fails
even when the translator's own unit tests still pass.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>

* fix(cli): keep markdown streaming prop stable to stop settle flash (#13719)

Flipping the <markdown> streaming prop from true to false when an
assistant text segment settles makes MarkdownRenderable call
updateBlocks(true), which skips every block-reuse path and destroys and
recreates all block renderables. Until tree-sitter re-highlights them
the whole message renders blank/unhighlighted, which users see as the
text flashing at the end of each response. Keep streaming={true} for
the transcript markdown (opencode's TUI does the same); entry.streaming
still drives the spinner glyph.

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

* Clarify model-facing message when user rejects a tool call (#12673)

* Clarify model-facing message when user rejects a tool call

* Include the rejected tool's name in denial reasons

* Move user-rejected tool reason into @cline/shared

* Route new user-rejection approval paths through shared reason builder

Since the original PR, several new approval surfaces landed on main with
their own terse denial strings (CLI connectors, ACP permissions, Cline Hub
webview, desktop webview, example VS Code extension). Route all of them
through buildUserRejectedToolReason so the model sees a consistent,
non-error rejection message.

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

* Add buildUserRejectedToolReason to the @cline/shared integration-test stub

The VS Code integration tests run the tsc-built CJS tree and stub the
ESM-only @cline/shared package in test-setup.js; the stub was missing the
new export, so tool-approval-denial.js threw at module load in CI.

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

* Trim scope back to the minimal rejection-copy fix

Restore the connector deniedReason plumbing, ACP permission strings,
desktop webview reason, example extension reason, and hub server fallback
to their main versions. Those surfaces already attribute the denial to a
user and are outside ENG-2329. Keep the Cline Hub webview change since
that path emits its own rejection string the model sees.

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

* Move rejection guidance suffix into agent runtime per review

* Apply review suggestions: neutral fallback reason and -- separator before rejection suffix

---------

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

* Default web search on for the desktop app (#13725)

* Default web search on for the desktop app

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

* Make desktop web search default seed best-effort

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

---------

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

* fix(desktop): enable macOS voice input (#13741)

* Promote ClinePass across home banner, account page, and settings (#12556)

* feat(webview): promote ClinePass across home banner, account page, and settings

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

* fix(webview): drop removed ext-cline-pass flag gating and hardcoded pricing from ClinePass promos

The ext-cline-pass feature flag no longer exists (the provider is ungated on
main), so promo surfaces are now gated only on self-hosted mode and org
remote-config provider allowlists. Promo copy describes the subscription
without a hardcoded price, matching the CLI copy cleanup in #13514.

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

* fix(webview): open the personal dashboard context from ClinePass subscription links

ClinePass always bills the personal account, but the Manage Subscription
button (and the ClinePass provider's usage link) landed org-context users
on the org dashboard. Pass personal=true like EntitlementError and the
CLI subscription links already do.

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

---------

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

* Desktop marketplace: show detail panel only on item click, left-align detail content (#13747)

* Desktop marketplace: show detail panel only on click, left-align detail content

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

* Desktop marketplace: drop license cell, single Learn more link (homepage, else repo)

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

* Desktop marketplace: keep selected entry open while list is filtered

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

---------

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

* feat(desktop): import sessions from Claude Code, Codex, and opencode (#13744)

* feat(core): session import service for Claude Code, Codex, and opencode history

Adds a SessionImportService to @cline/core that discovers sessions in the
on-disk stores of Claude Code (~/.claude/projects JSONL), Codex
(~/.codex/sessions rollouts + session_index titles), and opencode
(opencode.db sqlite), translates each conversation into Cline's native
MessageWithMetadata format, and persists it through CoreSessionService as
a completed, listable, resumable session.

Key mechanics:
- Claude Code: parentUuid tree walk from the newest leaf picks the active
  branch (edits/retries branch the log); same-message.id assistant lines
  merge back into one turn; sidechains, meta lines, and slash-command
  wrappers are excluded; ai-title/summary lines provide titles.
- Codex: real prompts come from user_message event_msg lines (user-role
  response_items are injected AGENTS/environment context, with a fallback
  for old rollouts); function_call/output pairs map to tool_use/tool_result;
  resumed rollouts that re-embed the original session id dedupe to the
  richest file; token_count events stamp per-turn metrics.
- opencode: reads a temp snapshot of the WAL-mode db; inline tool parts
  split into tool_use + tool_result to preserve provider-valid structure;
  child (subagent) sessions and synthetic parts are skipped.
- Shared sanitizer guarantees replayability: orphaned tool_use gets a
  placeholder result, orphaned tool_results and empty text blocks drop,
  provider-session-scoped signatures/encrypted reasoning strip.
- Imported sessions pass every history-visibility gate (terminal status,
  non-empty provider/model, chat-workspace fallback cwd, no fabricated
  checkpoint metadata) and carry metadata.importedFrom for idempotent
  re-discovery (alreadyImportedSessionId).

* feat(desktop): sidecar commands for importing sessions from other tools

Adds two sidecar WebSocket commands backed by @cline/core's
SessionImportService:

- list_importable_sessions: returns { installedTools, sessions } where
  sessions are ImportableSessionSummary rows (tool, sourceId, title, cwd,
  timestamps, messageCount, preview, alreadyImportedSessionId) discovered
  in the local Claude Code / Codex / opencode stores.
- import_sessions: takes { selections: [{ tool, sourceId }] }, validates
  each selection against the known tool list, imports sequentially
  (per-session transactional), and broadcasts session_import_progress
  events ({ index, total, result }) so the UI can render live progress.
  Returns { results } with per-item ok/sessionId/title/error.

* feat(desktop): import sessions UI for Claude Code, Codex, and opencode

Adds an Import Sessions dialog to the desktop app driven by the sidecar's
list_importable_sessions / import_sessions commands:

- Scan phase discovers local history from all three tools and groups it
  per tool with select-all checkboxes, per-row title, relative time,
  message count, and workspace folder; rows already imported are disabled
  and badged (idempotent re-open).
- Text filter across title, folder, and first-prompt preview.
- Import phase streams session_import_progress events into a progress bar
  and per-item result list; the dialog cannot be dismissed mid-import via
  overlay click. Done phase summarizes successes and lists failures with
  their error messages.
- Entry points: an Import button in the Sessions view header and an
  "Import sessions" row in Settings → General.
- use-session-history subscribes to session_import_progress so history
  refreshes no matter which surface started the import.
- Wire types live in webview/lib/session-import.ts (mirrors the core
  module's types so the client bundle never imports node-only code).

* fix(desktop): import dialog crash rendering session timestamps

formatRelativeTime takes a string (parseTimestamp calls .trim() on any
truthy value), but the import dialog passed the numeric updatedAtMs,
crashing the page with 'e.trim is not a function' as soon as scanned rows
rendered. Convert to an ISO string at the call site.

Slipped through because the webview has no typechecking anywhere:
tsconfig.dev.json excludes webview/ and next.config sets
typescript.ignoreBuildErrors, and the webview's own tsconfig currently
carries 64 pre-existing errors.

* feat(desktop): offer session import during onboarding

Adds an 'import' onboarding step between connect/github and done. The
step scans for importable Claude Code / Codex / opencode history on
entry and silently advances when nothing (new) is found or the scan
fails, so only people with actual history from other tools ever see it.
When sessions are found it summarizes the count and source tools, opens
the same ImportSessionsDialog used by the Sessions page for picking, and
flips to a confirmation state once at least one session imports. Skip is
always available, including while the scan is still running.

* fix(desktop): import dialog text overflow, collapsible sections, select all

- Titles no longer clip or push the row wide: they word-wrap up to two
  lines (line-clamp-2 + break-words, with min-w-0 down the flex chain so
  long unbroken Codex prompt titles can actually shrink); the meta line
  keeps time/count fixed and truncates only the workspace name; progress
  rows get the same min-w-0 treatment.
- Each tool section header is now a collapse toggle (chevron +
  aria-expanded) so one tool with hundreds of sessions doesn't force
  scrolling past it; collapsed headers still show count and selected
  count, and filtering forces sections open so search matches can't hide
  in a collapsed group. Collapse state resets per dialog open.
- New global Select all row above the list with indeterminate state and
  an x-of-y selected counter; it operates on the currently visible
  (filtered) selectable sessions, matching the per-section checkboxes.

* fix(desktop): import dialog header and search clipped by intrinsic column width

The dialog grid used the default auto column track, so a single
unbreakable string in a session title (Codex titles often contain URLs)
set the column's min-content width wider than the fixed 620px dialog --
break-words affects layout but not intrinsic sizing -- and
overflow-hidden then clipped everything in the column, including the
description and the search field. Pin the column to minmax(0,1fr) so the
container width always wins and long words wrap at the box edge instead.

Also add sm:max-w-none (the primitive's sm:max-w-lg survives
tailwind-merge across variants and was silently capping the dialog at
512px) and shrink-0 on the search and select-all rows so a tall list can
never compress them vertically.

* fix(desktop): onboarding import step rescanned after import and looped to done screen

The import step's scan effect depended on onContinue, an inline arrow the
parent recreates every render — and importing itself re-renders the app
shell via the history refresh. Each re-render re-ran the scan, and when
the user had imported everything (select all), the re-scan found zero
remaining sessions and hit the nothing-to-import auto-advance, yanking
them past their own import confirmation onto the done screen. The scan
now runs exactly once per step entry (onContinue held in a ref for the
async auto-skip paths).

Also, after a successful import the button is now 'Start building' and
completes onboarding directly instead of routing through the separate
done screen — two consecutive confirmation screens read as a loop. The
skip and nothing-found paths still go through the done screen so those
users get the 'You're all set' confirmation.

* fix(core): consolidate imported tool_results into the message after their tool_use

The import sanitizer answered missing tool_use ids with a separate
placeholder user message while leaving real results for the same turn in
later user messages. Anthropic requires every tool_result for a turn in
the user message immediately following it, so a partially-answered turn
would still 400 on resume. Rebuild any incomplete or split span as one
consolidated results message in tool_use order (placeholders for missing
ids, duplicates dropped) followed by a message carrying whatever else the
span held, mirroring the legacy migration sanitizer.

* fix(desktop): imported sessions resume on the user's configured provider; batch adapter caches

Opening a history session adopts the row's provider/model
(use-chat-session: session.provider || prev.provider), so imported rows
stamped with the source tool's provider — openai-native for Codex,
whatever opencode reported — resumed on providers the user may never have
configured and failed on first send. The dialog now passes the app's
current model selection (lastProvider/lastModelByProvider, i.e. what a
new chat would run on) and the service stamps it on the row; both halves
must be present so a Cline provider is never paired with a foreign model
id. The source provider/model are preserved in metadata.importedFrom and
per-message modelInfo stays accurate. Codex's provider id is corrected to
Cline's openai-native, and opencode's openai/google map to
openai-native/gemini.

Adapters also gain per-batch caches released via dispose(): Codex's
convert() re-walked the sessions tree and re-read every rollout head per
imported session (O(sessions x files)); it now builds the session-id ->
richest-file index once per batch. opencode copied the whole WAL db per
imported session; it now snapshots once per batch.

* fix(core): roll back failed imports and dedupe at import time

Addresses both Greptile P1s on #13744:

- A write failing after createRootSessionWithArtifacts (messages, status,
  manifest, title) left a half-written pid-0 session in history whose
  importedFrom marker also blocked retrying the source. persistConverted
  now deletes the session on any later failure and rethrows.
- Dedup markers were read through listSessions, which caps its scan at
  2000 rows, so a prior import older than the newest 2000 sessions was
  invisible and the source could be imported again. Add
  listSessionMetadata (ids + metadata for every row, no manifest reads or
  reconciliation) and use it for markers. Also check idempotency at
  import time, not only at discovery: a request for an already-imported
  source resolves to the existing session (alreadyImported: true) instead
  of writing a copy, covering stale pickers and repeated requests.

* fix(core): create imported sessions terminal and mark them imported last

Two failure modes shared one root cause -- the import wrote its session
in stages and claimed success too early:

- The row was created running/pid-0 and flipped to completed afterwards.
  The stale-session reconciler runs in the hub daemon against the same
  SQLite DB and, in that window, marks such rows failed and stamps
  terminal_marker metadata. createRootSessionWithArtifacts now accepts
  status/endedAt/exitCode so imports are created completed with the
  source session's end time; the separate status flip and manifest
  rewrite are gone.
- The importedFrom marker was written at creation, so a session whose
  later writes failed (and whose rollback delete also failed) still
  blocked retrying its source. The marker is now the final write, so it
  means 'this import finished' and a half-written session can never
  claim the source.

listSessionMetadata is unbounded by default so dedup sees every row.

* fix(core): resolve TS2352 casts in session-import tests (#13746)

tsc rejects casting ContentBlock[] straight to Record<string, unknown>[]
(RedactedThinkingContent is not comparable), which failed the Quality
Checks typecheck. Route the five assertion-site casts through a small
blocks() helper that widens via unknown.

* fix(core): flatten Codex content-block tool outputs during import

Newer Codex rollouts write custom_tool_call_output.output as an array of
Responses-API content blocks ({type:"input_text", text}) instead of a
plain string. The importer JSON.stringified that array into the
tool_result content, and the chat UI's tool-summary parser then rendered
each non-text block as its type label, so imported exec calls showed up
as "[input_text][input_text]" with no output.

Concatenate the text of string/text-bearing blocks (they are stream
chunks, so no separator) and keep the JSON fallback for anything else.

* fix(desktop): edit-and-resend on runs without a checkpoint

Editing a message forks the session before that run, and the sidecar
always routed that through manager.restore with workspace: true. Imported
sessions carry no checkpoint history, so editing any of their prompts
failed with "No checkpoint found at or before run N" — even after the
user had continued the session in Cline, since only the new runs get
checkpoints.

When no checkpoint exists at or before the edited run there is no
workspace state to roll back, so fork the trimmed transcript onto the
current workspace (the same path a full-history fork takes) instead of
erroring. Runs that do have a checkpoint still restore the workspace.

* fix(core): roll back failed session creation and coalesce overlapping imports

Two gaps Greptile flagged on the import path:

createRootSessionWithArtifacts upserts the row before writing the messages
file and manifest, and the call sat above persistConverted's rollback try.
A file write failing there left a completed row with no transcript in
history. Creation now runs inside the rollback, and deleteSession already
tolerates a missing row or missing files.

Each import_sessions request builds its own service and snapshots the
existing-import markers once, so two overlapping requests for one source
(a second window, a double-fired command) both passed the dedupe check and
persisted two sessions. A module-level in-flight map keyed by tool:sourceId
makes the later caller wait on the first write and report its session as
already imported.

* fix(desktop): resolve the import resume target like a new chat does

An imported Claude Code session resumed on the Anthropic provider instead
of the user's Cline selection. The dialog read model-selection storage
directly and required both a remembered provider and a remembered model;
the composer only records a model from the explicit picker handlers, so
anyone running on the default model has no entry, the lookup came back
empty, and the service fell back to the source tool's provider.

Resolve the target with getInitialChatConfig() -- the same chain a new
chat uses (remembered selection, then the built-in default), which is
never empty -- and have the import_sessions handler default to the cline
provider and CLINE_DEFAULT_MODEL_ID when a caller sends nothing, matching
other server-started sessions. The source provider can no longer become
the resume target.

* feat(desktop): group scheduled runs under their schedule in the sidebar (#13752)

* feat(core): stamp schedule id, name, and run number onto scheduled sessions

Sessions started by the cron runner only carried a generic
sessionHistoryOrigin.trigger = "hub-schedule", so clients could tell a
session was scheduled but not which schedule it belonged to or which run
it was. The runner now passes schedule provenance to the runtime handlers,
which merge it into the session metadata alongside the origin trigger:

  scheduleId          the hub schedule's external id
  scheduleName        the schedule title
  scheduleExecutionId the cron run id
  scheduleRunNumber   1-based position among every run created for the spec

The run number comes from a new SqliteCronStore.getRunOrdinal, which counts
runs of every status in creation order so a later cancellation never shifts
numbers already stamped onto earlier sessions. A reclaimed run keeps its
number, so two sessions with the same number make a duplicate visible.

HubScheduleRuntimeHandlers.startSession gains an optional second argument
carrying the metadata; existing implementations that ignore it keep working.

* feat(desktop): group scheduled runs under their schedule in the sidebar

A schedule that fires daily filled the sidebar's Scheduled section with a
row per run, each titled with the same prompt text, which read as if the
task had been duplicated. Runs of one schedule now fold into a single
collapsible row named after the schedule, with the run count on the right;
expanding it lists the runs as "Run N" sub-items (newest first) with their
usual status dot, time, hover card, context menu, and delete button. The
group holding the active session expands on its own so a run opened from
the Schedules page is visible. Grouping also applies inside project groups
when sorting by project. The Scheduled header now counts schedules rather
than runs.

Threads learn the schedule identity from the metadata the runner now
stamps (scheduleId, scheduleName, scheduleRunNumber). Runs recorded before
that fall back to the schedule executions list the hook already polls,
which now yields the schedule id and name instead of a bare session id set,
and finally to grouping by shared title. Runs without a number are labelled
with their start time instead of "Run N".

* fix(desktop): reopen a collapsed schedule group when one of its runs is opened

A stored collapse used to win over the active-session default for the
sidebar's lifetime, so a run opened from the Schedules settings page
could stay hidden inside its collapsed group. Opening a session now
clears the stored choice for the group that holds it; the group can
still be collapsed afterwards.

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

---------

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

* feat(cli): handle outdated hub sessions with drain and replace flow (#13727)

* feat(cli): handle outdated hub sessions with drain and replace flow

Add logic to detect when the CLI is newer than the running Hub and provide
users with options to either keep the older Hub running (to avoid
interrupting active sessions from other clients) or force-replace it.

Implement `describeOutdatedHubSessions` helper to show quantified session
activity in the dialog, and add `HubOutdatedContent` UI component with
detailed messaging for the `build_mismatch` case. The `unsupported_protocol`
case remains a modal requiring update, while the softer mismatch now uses
a toast with enter-to-replace or escape-to-keep choices.

Includes tests for draining and replacing an older busy hub when forced.

* fix(hub): gate desktop hub_upgrade behind trusted connection and make drain-first a hard guarantee

Address review: an originless local WebSocket client could invoke the
forceful hub_upgrade command, and a failed drain request still allowed a
forced retirement, so work started during the wait window could be killed.

- hub_upgrade now requires the same canApproveTools per-connection gate as
  the tool-approval commands.
- upgradeManagedHub skips the idle-wait window when the drain was not
  established (an undrained hub keeps admitting work, so waiting only
  widens the blast radius) and refuses to replace a busy hub that did not
  accept the drain, force or not. An idle hub is still replaced so
  pre-drain-endpoint hubs (404) remain upgradable.

* fix(hub): treat failed activity readings as unknown, not idle, during hub upgrade

A transient session.list failure inside the drain wait window previously
read as an idle hub, which could end the grace window early and authorize
retirement while turns were still finishing.

- Failed readings never end the wait window early, never overwrite the
  last real observation, and never authorize a non-forced retirement.
- Without force, a hub whose activity was never confirmed is handed back
  un-drained (still_busy) instead of retired; an undrained hub is now
  replaced only when positively observed idle.
- With force and an accepted drain, an unanswerable hub is still replaced:
  the user already consented to interrupting its sessions.

* fix(hub): never retire an undrained hub on an idle snapshot

An older hub that rejects the drain has no admission barrier, so a single
idle reading cannot authorize retirement: a session admitted right after
the snapshot would die in a retire the consent prompt never covered.

upgradeManagedHub now retires a hub only under an accepted drain. The
undrained-idle case is delegated to the locked ensure path, which
re-checks activity immediately before its own retire ladder and attaches
(deferring the swap) when new work arrived in the meantime; the upgrade
then reports still_busy instead of replaced, and the desktop/TUI surfaces
tell the user to retry.

* fix(hub): require an accepted drain unconditionally before any upgrade retirement

Review follow-up: the undrained-idle delegation still reached
retireDiscoveredHub, whose own drain attempt is best-effort, so a session
admitted after the idle re-check could die in the shutdown.

upgradeManagedHub now fails fast when the hub does not accept the drain -
no wait window, no idle exception, no delegation. The drain is the
admission barrier that keeps every subsequent reading true through the
retire; a hub too old or wedged to accept it is left to the automatic
ensure path, which replaces it once idle at the next client startup, and
the error says so.

* fix(hub): establish the drain barrier before the automatic idle check

Review follow-up: the automatic incompatible-hub path read session
activity first and drained only inside the retire ladder, so a session
admitted between the idle snapshot and the shutdown could be terminated.

retireIncompatibleHub now requests the drain before the busy check: with
the drain accepted, the idle reading stays true through the retire. A
deferred (busy) hub, and one whose retirement fails or is skipped by the
circuit breaker, gets the drain lifted so it never sits alive-but-refusing
work. Hubs that do not accept the drain (pre-/drain builds answer 404)
keep the historical best-effort snapshot rather than being stranded
forever.

* polish(hub): tighten the outdated-hub dialog copy

Two short sentences instead of four long ones, spell out what Quit Cline
does (closes the app, leaves the Hub running), and rename the action to
Update Now in both the desktop dialog and the TUI variant.

* fix(cli): show the keep-Hub reminder toast when the outdated-hub dialog is dismissed (#13754)

dialog.choice() resolves undefined on Esc rather than rejecting, so the
reminder toast in .catch() never ran. Move it to the falsy branch of
.then(), matching the unsupported_protocol handler.

---------

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

* chore(sdk): release v0.0.82

* chore(cli): release v3.0.61

* fix(core): close imported-session stores before the temp dirs are removed

The session-import tests opened a SqliteSessionStore per case and never
closed it, so afterEach's rmSync ran against a directory still holding an
open SQLite file. POSIX allows that; Windows does not, and all seven
persisting cases failed the sdk-publish Windows job with EPERM on the
cline-db-* temp dir.

Route every store through a sessionStore() helper that registers it for
close, and close them before removing the dirs.

* chore(vscode): release v4.1.17 (#13755)

* chore(desktop): release v0.0.22

* fix(standalone): decode core-connection protobus requests from proto3 JSON (#13758)

The core connection delivers protobus requests as the proto3 JSON the
webview's ts-proto toJSON encoders produce: enums arrive as string names
and default-valued fields — empty repeated fields included — are omitted.
The handlers assume ts-proto message shapes (numeric enums, repeated
fields always present), so dispatching the parsed JSON directly broke
every RPC relying on those invariants on JetBrains: changing the API
provider threw 'Cannot read properties of undefined (reading length)'
in fromProtobufModelInfo, and the plan/act toggle rejected its own mode
as invalid. The old standalone gRPC server restored these defaults
during protobuf decoding; the tunnel skipped that step.

Generate a per-method request-decoder map (request type fromJSON)
alongside the service handlers and apply it in the core-connection
dispatcher before dispatch. The in-process VS Code webview path is
untouched: it posts structured-cloned ts-proto objects that never pass
through JSON.

* feat(core): Hub-managed Agent Plugins support (#13652)

* feat(sdk): add hub-managed Agent Plugins

* fix(sdk): restrict Agent Plugin auto-discovery

* test(sdk): canonicalize Windows plugin paths

* fix(sdk): await stdio MCP process shutdown

* fix(sdk): select Agent Plugin MCP clients by source

* fix(core): defer Agent Plugin data directory creation

* docs(sdk): clarify Agent Plugin discovery scope

* fix(sdk): reject individual Agent Plugin skill toggles

settings.toggle({type: "skills"}) unconditionally called
toggleSkillFrontmatter() for any resolved skill record, including ones
sourced from an Agent Plugin. That writes a `disabled` key into the
skill's SKILL.md frontmatter, but the strict Agent Skills parser used
for these skills only permits a closed field set (name, description,
license, compatibility, metadata, allowed-tools). The very next reload
then rejects the file as invalid and the skill silently disappears
until someone hand-edits the installed plugin's SKILL.md.

Guard the toggle: an agent-plugin-sourced skill record now throws a
clear error pointing at the plugin-level toggle instead, matching how
whole-plugin enable/disable already works (setDisabledAgentPlugin,
keyed by manifest name, no file mutation).

* fix(sdk): keep disposing MCP servers when one disconnect fails

InMemoryMcpManager.dispose() unregistered servers sequentially and let
the first disconnect() rejection abort the loop. Since disconnect() can
now reject when a stdio child never exits, one wedged server would leak
every remaining server's process. Catch per-server errors, disconnect
the rest, and rethrow as an AggregateError so upstream cleanup-error
reporting still sees the failure.

Also log agent plugin discovery failures in CoreSettingsService.list
instead of swallowing them silently, so a plugin missing from settings
is diagnosable.

* feat(cli): manage Agent Plugins through the Hub (#13657)

* feat(desktop): manage Agent Plugins through the Hub (#13658)

* feat(desktop): manage Agent Plugins through the Hub

* fix(desktop): show Agent Plugin inventory

* docs: add deprecation notices page (#13458)

* docs: add deprecation notices page

* docs: add primary surface to deprecations

* chore: drop unrelated formatting changes from docs PR

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>

* fix(desktop): show the newer-hub dialog only when an app update is staged, and persist Later (#13787)

* fix(desktop): show the newer-hub dialog only when an app update is staged, and persist Later

Hardens the 'Cline Hub was updated' prompt against release skew:

- The build_mismatch modal renders only when the auto-updater reports a
  staged update ('ready'), so it can never loop on 'no app update is
  available yet'. A mismatch kicks one immediate updater check (deduped
  per hub build per page lifetime) so the prompt opens actionable as soon
  as a release exists, and the shared polled status opens it reactively
  when the background cycle stages one later. unsupported_protocol and
  outdated_hub keep their unconditional dialogs.
- 'Later' now persists in localStorage per reason:hubBuildId. The sidecar
  replays a pending mismatch on every webview connection (session
  switches, reloads, relaunches), and the previous in-memory dismissal
  resurrected the modal on each one. A different hub build still prompts.

* fix(desktop): never persist Later for an unsupported-protocol hub

Review follow-up: the persisted dismissal also stuck for
unsupported_protocol, silencing a warning about a Hub the app genuinely
cannot talk to across every reconnect and relaunch. Dismissal for that
reason is session-local again (the pre-existing behavior); only the
advisory build_mismatch key persists, enforced on both write and read so
a key stored by any other path is ignored too.

* fix(desktop): reopen a dismissed protocol warning when the mismatch is redelivered

Review follow-up: an in-place transport reconnect replays the pending
mismatch to a still-mounted dialog whose in-memory dismissedKey is
unchanged, so a dismissed unsupported_protocol warning stayed closed
while the app could not talk to the Hub.

Every delivered mismatch now passes the dismissal through
retainDismissalForIncomingMismatch: a matching non-persistable dismissal
(unsupported_protocol) is cleared so the warning reopens on the replay;
the advisory build_mismatch dismissal and dismissals for unrelated keys
stand.

* fix(hub): never prompt about a hub running the same core version (#13785)

Two artifacts of the same release cut from different commits never share
a build fingerprint or epoch: desktop-v0.0.22 and cli-v3.0.61 both bundle
core 0.0.82, yet every desktop user with the CLI installed gets the
'Cline Hub was updated' dialog on every launch and webview reconnect, and
'Update and restart' loops on 'no app update available' because nothing
newer exists to install.

checkManagedHubBuildMismatch now returns nothing when the hub's
coreVersion equals this client's own, in both directions (build_mismatch
and outdated_hub). The fingerprint keeps its role in the reuse/retire
total order, where antisymmetry matters; it no longer drives prompts on
its own. Genuinely different releases still prompt.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: JasmineLCY <38378321+JasmineLCY@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Haley Park <haleypark.design@gmail.com>
Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>
Co-authored-by: yzxcj797 <54314860+yzxcj797@users.noreply.github.com>
Co-authored-by: yzxcj797 <yzxcj797@users.noreply.github.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: 𝓜𝓲𝓼𝓼𝓪𝓻𝓲 𝓐𝓱𝓲𝓵 🌿 <143264692+missarii@users.noreply.github.com>
Co-authored-by: Dominic Cooney <dominic.cooney@cline.bot>
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Harrison <harrison@cline.bot>
Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: TheRealSpencer <32678829+TheRealSpencer@users.noreply.github.com>
Co-authored-by: Etisha Garg <etisha.garg@cline.bot>
2026-09-02 18:19:33 -07:00
+15 b330eef7f4 chore(desktop): sync latest main into desktop experimental (#13780)
* fix(vscode): continue the surviving session on resume instead of rebuilding with the original task text (#13175)

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilt the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

The preserved conversation history is the source of truth on resume, so
the fallback prompt now just asks the model to reassess the history and
continue, matching the legacy resume prompt which also never resent the
original task. User-typed text still takes precedence when provided.

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

* fix(vscode): continue the surviving idle session on follow-ups instead of rebuilding

Stopping a turn keeps the session alive, but every idle follow-up (bare
Resume after Stop, and typed follow-ups after a completed turn) tore
that session down and rebuilt it from persisted task history before
sending. Continue the matching idle session in place instead, the same
way the CLI reuses the live session after an abort. Rebuilding from
history now only happens when no live session matches the displayed
task (task opened from history, extension host reload).

A bare resume still needs a prompt to start a turn, so it sends the
neutral [TASK RESUMPTION] prompt (shared with the rebuild fallback and
hidden from the transcript); user-typed content is echoed and sent
as-is. If the send lands while the abort is still settling, the runtime
auto-queues it and drains once the abort completes.

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

* refactor(vscode): consolidate follow-up send paths in SdkFollowupCoordinator

Now that idle follow-ups continue the live session in place, the
two-mode sendToActiveSession helper was redundant: its non-queued branch
duplicated continueIdleSession minus the bare-resume prompt. Split it
into a single-purpose queueToActiveSession and fold the idle no-task
send into continueIdleSession, flattening askResponse's decision tree
to: queue onto a running turn, continue a matching live idle session,
rebuild from history, or abandon.

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

* refactor(vscode): reuse the existing neutral resumption prompt for bare resumes

Drop the newly invented long resumption wording in favor of the phrase
that already existed as the no-history fallback and that the transcript
hiding logic and test fixtures recognize: '[TASK RESUMPTION] Please
continue where you left off.' The net change to resumeSessionFromTask
against main is now just deleting the branch that resubmitted
historyItem.task as new instructions.

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

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilds the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

Bare resumes now always use the neutral prompt that already existed as
the no-history fallback; user-typed text still takes precedence. This
matches the legacy resume prompt (responses.taskResumption), which only
ever included user-supplied text as new instructions.

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

* fix(vscode): hide synthetic prompts from the queued-prompt echo

A send that races a settling abort is auto-queued by the runtime, so a
bare Resume can reach the pending_prompt_submitted echo carrying the
synthetic [TASK RESUMPTION] prompt. Echoing it leaked model-facing text
as a visible user bubble and shifted the visible-user-message ordinals
that edit/regenerate mapping relies on. Filter synthetic prompts with
isSyntheticUserPrompt, keeping user attachments visible (matching
isSyntheticSdkUserMessage semantics).

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

---------

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

* fix(vscode): preserve LiteLLM input token limits (#13293)

* fix(vscode): preserve LiteLLM input token limits

* fix(vscode): prefer live LiteLLM model metadata

* fix(vscode): generalize private catalog metadata

* test(vscode): preserve llms exports in vscode lm mock

* fix(vscode): point provider signup URLs at their API key pages (#13337)

* fix(vscode): point Mistral signup URL at the general API keys console

The Mistral provider's signup link led to the Codestral console, which
issues Codestral-scoped keys that fail with 401 on api.mistral.ai — the
endpoint the provider actually calls. Point it at the general API keys
page instead.

Fixes #13288

* fix(vscode): deep-link DeepSeek and Fireworks signup URLs to their API key pages

Both pointed at marketing homepages; link straight to the key-creation
pages instead, matching the rest of the registry and the desktop app's
provider-key-urls map.

* fix(ci): always build the legacy bundle from the legacy-extension branch (#13349)

The combined-VSIX workflow took legacy-ref as a free-form dispatch input
with no publish-time validation (next-ref has one: publish requires main).
Any typed ref — a PR merge ref, an unprotected branch — would be built
into the published VSIX by the environment-less build job, and the publish
environment approver only ever sees an opaque prebuilt artifact, so the
approval protected the marketplace PAT but not the shipped bytes.

Remove the input entirely and hardcode the protected legacy-extension
branch, which makes that branch's protection rules load-bearing for
releases. The tested-sha pinning between test-legacy and build is
unchanged. publish-extension skill dispatch command updated to match.

* fix(ci): lock the legacy publish workflow to the legacy-extension branch (#13350)

The branch dispatch input was a free-form string with no validation. Both
jobs checked it out and ran full npm lifecycle scripts from it: the publish
job next to VSCE_PAT/OVSX_PAT (and npm run publish:marketplace executes a
script from that same ref with the PATs in env), and the test job with NO
environment approval at all while inheriting the workflow-level
contents/packages/checks/pull-requests write grants. A dispatch pointing at
e.g. refs/pull/N/head would run outside-contributor code with the
marketplace keys behind one approval, or with a repo-write token behind
none.

Remove the input and hardcode the protected legacy-extension branch, drop
the workflow-level permissions to contents: read, and elevate only the
publish job to contents: write (tag push + GitHub release). The branch
input's default was legacy-extension, so normal publishes are unchanged.
publish-extension skill dispatch command updated to match.

* fix(vscode): SDK remote-config parity — refresh coordination, session gating, and fail-closed opt-out (#13226)

* feat(desktop): native notifications (#13166)

* feat(desktop): native notifications

* macos target

* fix(desktop): isolate macOS dev app identity

* fix(desktop): address notification review feedback

---------

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

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched (#13310)

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched

Toggling an auto-approve setting while a task is open writes
autoApprovalSettings into the StateManager's task-settings overlay
(updateAutoApprovalSettings -> setTaskSettings). The SDK controller never
cleared that overlay on clearTask/showTaskWithId (the legacy controller
did), so after New Task the stale overlay kept shadowing global settings
in getGlobalSettingsKey(): toggle RPCs were accepted into global state,
but every posted state still carried the overlay's old version, which the
webview rejects as not newer - the auto-approve checkboxes froze forever.

Restore legacy parity in SdkTaskControlCoordinator: drop the overlay
(persisting pending writes first) in clearTask() and before installing a
different task's proxy in showTaskWithId().

Fixes #13260

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

* changeset

* test(vscode): add end-to-end regression test for auto-approve freeze after New Task

Wires the real StateManager, the real updateAutoApprovalSettings handler,
and the real SdkTaskControlCoordinator.clearTask() together with the
webview's version gate modeled on ExtensionStateContext, pinning the
end-to-end invariant behind #13260: checkbox toggles must keep reaching
the webview after a mid-task toggle followed by New Task. Verified the
test fails when the clearTaskSettings() call is removed from clearTask().

* fix implicit any in regression test

---------

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

* feat(desktop): show provider web-search support under the settings toggle (#13328)

* feat(desktop): show provider web-search support under the settings toggle

The global Web search toggle silently does nothing unless the session's
provider offers native web search, which made the setting read as if it
worked with any provider. The desktop General settings row now explains
that only providers with built-in web search honor it, and shows a live
status line: which connected providers are ready to use it (no extra
setup needed), or an amber warning with a link to the Models section
when none of them support it.

Support is resolved in the webview via a new providerOffersModelTool
helper in @cline/llms (browser export), sharing the same builtin-manifest
source of truth as the runtime's supportsModelTool attachment check.

* fix(desktop): address review — refetch web-search status on catalog invalidation, clarify per-model support

Greptile P2: the one-time catalog fetch could race an in-flight provider
save and show stale status; the row now refetches when the provider
catalog cache is invalidated (fired after saves complete).

Greptile P1: the ready line implied every model on the provider works;
Vertex excludes Claude routes, so the copy now scopes the promise to
models that support it.

---------

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

* feat(ui/desktop): collapse finished runs into a work summary and remove hover-state dead space (#13315)

* feat(ui): add WorkActivity collapsed-run summary and float message actions as a pill

WorkActivity/WorkActivityTrigger/WorkActivityContent fold a finished agent
run's working rows (tool calls, thinking traces, narration) behind a single
"Worked for 4m 12s · 14 tool calls" disclosure built on the shared animated
disclosure primitives, with formatWorkActivityLabel/formatWorkDuration
exported for consumers.

Message hover actions no longer rely on the transcript reserving blank space
below each message: the action row is now a self-backed pill (border,
blurred background, shadow) that floats over whatever follows, so
conversations can pack rows tightly without hover chrome colliding with the
next message.

* feat(desktop): collapse finished runs into a work summary and tighten chat spacing

collapseCompletedWork post-processes the grouped transcript: once a run ends
on assistant text with no further tool calls, its working rows fold into one
expandable WorkActivity row while the final answer stays visible. Runs are
delimited by user messages; the trailing run only collapses when the session
has stopped running and actually produced an answer, so live streams and
cancelled/failed tails keep their rows. Assistant messages carrying images
or media are treated as deliverables and never collapse.

The conversation list gap drops from gap-8 to gap-4 now that hover actions
are self-backed pills that need no reserved space, and user messages add
their own top margin so turn boundaries stay visually distinct.

* refactor(ui/desktop): work summary label wording, flat expansion, stable in-run rhythm

Feedback round on #13315:

- Label reads "Worked for 4m 12s and made 14 tool calls" instead of joining
  with a dot; without a duration it falls back to "Made N tool calls".
- Expanded work rows render at transcript level — no rail or extra indent —
  since tool rows and thinking traces already carry their own nesting when
  expanded. The work content keeps the tight working-row rhythm.
- Live working rows (thinking traces + tool calls) now group into a 'run'
  render item with the same tight 0.25rem rhythm, so there is no oversized
  gap under a "Thought for Ns" row and every row keeps its exact position
  when the finished run folds into the work summary. A trailing
  answer-in-progress stays outside the group at transcript level, and pure
  prose spans keep normal spacing.
- The transient "Thinking..." indicator moves inside the transcript column
  and mirrors a trigger row's geometry, so the first real row replaces it in
  place with no jump.

* style(ui/desktop): hover-pill metrics, right-pointing work chevron, scroll and spacing fixes

Another feedback round on #13315:

- Hover action pill: +2px internal padding, a trailing inset after the
  timestamp (it sat flush against the pill border), and more clearance
  between the message content and the pill (2px -> 6px; the hover bridge
  grows to match).
- The work summary chevron points right while collapsed and continues
  counterclockwise to point up when expanded.
- Conversation bottom padding drops pb-20 -> pb-8: the composer sits below
  the scroller, so the padding only needs to clear a pinned action pill.
- Sending a message scrolls back to the bottom even if the reader had
  scrolled up (new AutoScrollOnSend on the user-message count, which ignores
  optimistic-bubble re-keying; @cline/ui now exports useConversation for
  this).
- An assistant answer directly under its run's working rows pulls itself
  0.5rem closer than the full transcript gap.

* style(desktop): leave a visible gap between a pinned action pill and the composer

pb-8 exactly matched the pill's ~40px footprint, so the last row's hover
actions sat flush against the composer top; pb-12 restores ~8px of daylight.

* style(desktop): widen the gap between the pinned action pill and the composer to ~24px

pb-12 left only ~8px of daylight under the pill; pb-16 reads comfortable
without reverting to pb-20's dead space.

* fix(desktop): keep the thinking indicator at the working-row offset mid-run

The indicator matched a trigger row's geometry but sat a full transcript gap
(1rem) below the last working row, while the tool/thinking row replacing it
joins the tight run group at 0.25rem — a visible upward jump. When the last
transcript item is working rows (or streamed assistant output), the
indicator now pulls up to the same tight offset; only at the start of a run,
under the user message, does it keep the normal gap.

* style(ui): calm the hover actions surface per team feedback

Borderless rectangle instead of the bordered pill: radius drops to
var(--radius), the side padding goes entirely (the icon buttons carry their
own hit areas), and the vertical padding halves. Blurred background and
shadow stay so it remains legible over following content.

* feat(ui/desktop): full-band hover reveal and iOS-style disclosure easing

The hover actions only appeared while the pointer was inside the message
box itself. The invisible bridge under each message now spans the full
height of the band the floating actions occupy (full row width), so
hovering anywhere in that strip reveals them. Sibling row types
(.cline-chat-tool, .cline-chat-work, and the desktop's run/tool groups)
become position: relative so they paint above the bridge — their own
content keeps its hover and clicks, and the bridge only wins in the band's
genuinely empty space.

All expandable rows (work summary, tool panels, thinking) open and close on
a 240ms symmetric ease-in-out cubic-bezier instead of the 60ms snap, with
chevron rotation on the same curve. Reduced-motion still disables both.

* revert(ui/desktop): drop the full-band hover reveal; quicken disclosure easing to 180ms

The full-band hover bridge (and the position: relative changes that made it
safe) is reverted per feedback — back to the narrow bridge that only spans
the gap under the message. The iOS-style ease-in-out on disclosures stays
but speeds up from 240ms to 180ms.

* fix(ui): recover live tool diffs that mount as a blank pierre skeleton

Live-streamed edit rows could show an empty diff for the whole run, with the
diff only appearing after the collapsed work row was expanded (fresh mount).
Root cause, confirmed by driving a live session and inspecting the element:
React StrictMode double-invokes @pierre/diffs' ref callback; the first
instance's async highlight work aborts on its immediate cleanup, and the
second instance adopts the abandoned half-rendered shadow tree as if it were
complete prerendered output — zero height, no code, no theme stylesheet,
permanently. A rendered diff always carries style[data-theme-css] in its
shadow root, so ToolFileDiff now checks for it shortly after mount and
remounts FileDiff (bounded attempts) when missing; the fresh host element
takes the normal render path and recovers within ~400ms. Verified live: the
diff now renders during the run.

* fix(desktop): keep interrupted runs expanded even with partial trailing text

The trailing-run collapse gated on 'ended with assistant text', which
misread a Stop that landed mid-answer as a finished run and folded the tool
calls the user wants to inspect. The gate is now the terminal status itself:
only completed (or restored-idle) sessions collapse the trailing run;
cancelled/failed/error tails keep their rows regardless of partial text.
(Greptile P1 on #13315 — matches the PR's stated rule.)

* feat(ui): share the markdown pipeline, chat polish, and ThinkingBlock across products (#13323)

* feat(ui): share the markdown pipeline, chat polish CSS, and ThinkingBlock

The desktop app and the cloud dashboard both consume @cline/ui yet rendered
assistant output differently, because Markdown policy and the thinking-trace
row lived app-side. This moves the shareable parts into the package:

- components/markdown (new export): the lazy Shiki code highlighter (GitHub
  light/dark, pinned language set) and agentMarkdownControls — the standard
  Streamdown configuration. streamdown/shiki/@shikijs/* become optional peer
  dependencies, mirroring @pierre/diffs.
- components/markdown.css: the desktop's chat polish moves in — chat-scale
  headings, outside list markers, single quiet code blocks with a
  hover-revealed copy control, table cards. Kept unlayered so it beats
  Streamdown's layered Tailwind utilities without !important.
- ThinkingBlock + formatThoughtLabel in agent-chat: the standard thinking
  row (brain icon, Thinking/Thought-for-Ns label, streaming shimmer, rail
  presentation, capped scrollable body). The shimmer and the
  reasoning-hover-suppression rule move into agent-chat.css; triggers gain
  the color transition the desktop applied locally.

Version bumps to 0.2.0-next.5 for the dashboard to pick up.

* refactor(desktop): consume shared markdown and thinking primitives from @cline/ui

The local Shiki highlighter, Streamdown controls, chat markdown polish CSS,
streaming-title shimmer, and reasoning hover-suppression rule are deleted in
favor of the @cline/ui versions (the highlighter test moves to the package's
suite). ReasoningBlock becomes a thin wrapper that hands MemoizedMarkdown to
the shared ThinkingBlock, and formatThoughtLabel re-exports from the package
so grouping code and tests keep their import path.

globals.css now imports @cline/ui/components/markdown.css (unlayered, so the
polish keeps beating Streamdown's layered utilities); the app keeps only what
is genuinely app-specific: link/image policy in markdown.tsx, selectability
rules, accent palettes, and the view-enter transition.

* style(ui/desktop): make thinking-trace prose legible

Thinking body text rendered too faint: plain muted-foreground plus the
desktop's font-thin weight. The shared thinking content now leans 75% of the
way back toward the body text color (still slightly de-emphasized), and the
desktop drops the thin font weight.

* ci(ui-publish): build @cline/shared before ui typecheck (#13354)

@cline/ui's generated-media imports @cline/shared/browser, which resolves to
shared's dist output. The build-shared step sat after typecheck/test/build,
so the first ui-publish dispatch since #13025 failed at Typecheck UI with
TS2307. Move the step to right after install.

* fix: run_commands object form without args routes through the shell instead of failing with ENOENT (#13336)

* fix: run_commands object form without args routes through the shell

The structured { command, args? } form of run_commands was always spawned
directly with shell: false. When a model emitted a full command line in
command with no args (e.g. { command: "echo hello" }), spawn failed with
ENOENT for any command containing a space, breaking command execution for
the whole session.

Direct exec now only applies when a non-empty args list is provided; the
object form without args is routed through getShellInvocation like the
string form. Schema descriptions are tightened so models put arguments in
args instead of embedding them in command.

Fixes #13279

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

* chore: trim structured-command schema descriptions

The union schema is only used for lenient validation of input the model
already sent; its descriptions never reach a model prompt. Keep them
short instead of restating executor behavior.

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

* chore: simplify direct-exec comment in shell executor

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

* revert: keep original structured-command schema description

The description never reaches a model prompt and the executor now handles
both shapes, so the wording change was cosmetic noise.

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

* fix: gate direct exec on args key presence, not array length

Review feedback: an explicit empty args array is intentionally structured
input and stays direct exec; only an object with no args key is treated
as a full shell command line. Matches the key-presence rule already used
by the VS Code host's formatCommandForTerminal. Also replaces the
empty-args shell test (which was PowerShell-incompatible) with a test
pinning the direct-exec contract.

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

---------

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

* fix: normalize Gemini custom base URLs for legacy host-root values (#13329)

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

* docs: add GLM-5.3 to ClinePass models and reference pricing (#13357)

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

* feat(desktop): stream run command output (#13179)

* feat(desktop): stream run command output

* fix(sdk): clean up detached command logs

* fix(sdk): reap detached logs after hub restarts

* fix(sdk): preserve live detached command logs

* fix(desktop): harden live command progress

* fix(sdk): recover detached logs for local hosts

* fix(desktop): reconcile command output tool rows

* fix(sdk): retain logs for surviving commands

* fix(core): prevent PID reuse from retaining detached logs

* fix(core): preserve detached logs on probe failures

* fix(core): retain detached logs during probe outages

* fix(desktop): resolve leftover merge conflict in messages projection test

Combine both sides of the assertion: main's incremented per-block
createdAt projection and this branch's toolCallId/hookEventName meta.

---------

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

* fix(cli): make TUI dialog colors follow theme changes live (#13355)

* fix(cli): make TUI dialog colors follow theme changes live

Dialog content previously read the static palette constant, so open
dialogs (including the theme picker itself) kept the default dark-blue
accents while scrolling through theme previews. Add getDialogPalette /
useDialogPalette, which resolve dialog colors from the active theme's
dialog accents and re-render on every theme change, and migrate all
dialog-rendered components to it.

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

* feat(cli): derive dialog panel background from the active theme

Dark themes now lift their own background one OKLAB step for the dialog
surface, so panels keep the theme's hue instead of the library's fixed
#262626. DialogThemeSync pushes the surface into the dialog container
for new dialogs and repaints open panels, so the surface also follows
live theme previews. Light themes keep the neutral dark panel to match
the dark accent fallback and the light-on-dark dialog text.

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

---------

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

* fix: skill slash commands load via the skills tool instead of expanding into the user message (#13327)

* fix(desktop): show typed slash command instead of expanded skill markdown

The sidecar expands /skill and /workflow tokens into their instructions
before dispatching, so the runtime's persisted transcript only contains the
expanded text. After a turn (and when reopening a session) the webview
re-hydrates from that history and rendered the whole SKILL.md body as the
user's message; queue events echoing the expanded prompt could also add a
second user bubble, and fresh sessions were titled with the markdown's first
line. The CLI never shows this because its TUI keeps the typed text in its
own transcript and only sends the expanded prompt to the model.

Mirror that separation inside the desktop sidecar's display boundaries:

- history projection (readSessionMessages) inverts user text that starts
  with a configured command's instructions back to '/name remainder',
  which also repairs sessions recorded before this fix
- queue snapshots and chat_queued_prompt_start events echo the typed
  prompt recorded at expansion time, so the webview's optimistic-bubble
  re-key matches again
- an untitled session sent an expanded prompt gets titled from the typed
  command instead of the instructions' first line

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

* fix(desktop): don't overwrite a mid-turn rename with the typed-command title

The untitled check ran before dispatch, so renaming a fresh slash-command
session while its first turn was running got clobbered by the post-turn
typed-command title. Re-check at write time and only replace a missing title
or the one the runtime auto-derived from the expanded prompt.

Also documents the inherent prefix-inversion ambiguity flagged in review:
text hand-typed with a command's exact instructions persists byte-identically
to that command's expansion, so stored history alone cannot distinguish them.

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

* fix(desktop): stop expanding skill commands; let the skills tool load them

Pasting the skill body into the prompt is why the transcript could ever show
it: the desktop webview re-hydrates from the runtime's persisted history, so
whatever the sidecar splices into the user message renders as if the user
typed it. The runtime already registers the skills tool, whose description
requires the model to invoke it whenever the user references a slash command
— so send the typed /skill text through and let the tool deliver the
instructions as a tool result (previously they arrived twice: pasted and via
the tool). The persisted user message, session title, and queue entries are
then simply the typed command, which deletes the typed-prompt registry, the
queue event/snapshot rewriting, and the title machinery from the previous
approach.

Workflows are not served by the skills tool and keep textual expansion, so
the read-time display inverter stays: it collapses expanded workflow prompts
— and skill prompts persisted before this change — back to the typed
/command in the history projection.

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

* feat(core): option to keep skill slash commands typed for the skills tool

resolveRuntimeSlashCommandFromWatcher (and the hub snapshot proxy) accept
expandSkillCommands: hosts whose sessions register the skills tool pass
false so the typed /skill goes through and the model loads the instructions
as a tool result, keeping the persisted transcript as what the user typed.
Workflows always expand — the tool does not serve them. isSkillsToolAvailable
exposes the catalog check hosts use to decide (yolo preset and the skills
tool toggle leave textual expansion as the only delivery path).

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

* fix(cli): skill slash commands load via the skills tool instead of expanding

The TUI user-command wrap and buildUserInputMessage now keep a typed /skill
as-is when the session's mode/toggles register the skills tool, matching the
desktop app; workflows keep expanding, and yolo (zen) keeps expanding skills
because its preset has no skills tool. This also fixes CLI resume/history
surfaces showing the skill body: the persisted user message is now the typed
command.

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

* fix(vscode): keep configured skill slash commands typed for the skills tool

expandSlashCommands no longer splices a configured skill's instructions into
the model text; the SDK session's skills tool delivers them as a tool result
(previously they arrived twice). Builtin pseudo-skills like /deep-planning
are not served by that tool and keep expanding, as do workflows.

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

* refactor(desktop): use the shared skill-expansion option in the sidecar

Replaces the sidecar's workflow-detection dance with core's
expandSkillCommands option and gates on isSkillsToolAvailable, restoring
textual expansion where the tool is missing (yolo mode or the skills tool
toggle) — a gap in the previous desktop-only change.

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

* refactor(desktop): drop the display inverter for expanded transcripts

Accepted trade-off to keep the change minimal: sessions recorded before
skills switched to the skills tool, workflow sends (deprecated), and
yolo-mode skill sends persist expanded instructions and now render that text
as-is instead of being collapsed back to the typed /command at projection
time.

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>

* Use fixed selection chevron in account dialog to match other dialogs (#13364)

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

* fix(desktop): align system prompt with session mode (#13361)

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

* fix(desktop): finalize queued turns on chat_done with canonical history reconcile (#13330)

Turns that settle through the event stream (queued prompts, including the
first prompt of a fresh session) resolve their send() RPC early, so nothing
cleared the streaming shimmer or reconciled live-streamed content against
the persisted transcript at turn end. A turn whose deltas were incomplete
stayed visually streaming forever and only healed when a later non-queued
send rehydrated history.

chat_done (and chat_session_ended / the queue-drain double check) now clears
the active assistant streaming id and schedules a short-delayed
read_session_messages + applyCanonicalHistory, guarded by turn epoch,
session id, and in-flight send submissions so it never clobbers a newer
turn or duplicates the blocking send path's own finalization.

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

* chore(desktop): release v0.0.14

* fix(clients): filter non-chat models from chat pickers (#13317)

* fix(clients): filter non-chat models from chat pickers

* fix(clients): align chat model eligibility

* fix(desktop): strip user_input envelope when copying a user message (#13369)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>

* docs: show DeepSeek V4 peak and off-peak pricing (#13312)

* docs: update DeepSeek V4 average pricing

* docs: show DeepSeek peak and off-peak pricing

* docs: add GLM-5.3 reference pricing (same as GLM-5.2)

* docs: add GLM-5.3 to ClinePass models table

* fix(llms): display billed gateway cost (#13385)

* fix(shared): run PowerShell commands with fail-fast error semantics (#13358)

* fix(shared): run PowerShell commands with fail-fast error semantics

The run_commands PowerShell wrapper never set $ErrorActionPreference, so
the default 'Continue' applied: a pipeline erroring per item (e.g. a
malformed Where-Object over Get-ChildItem -Recurse) emitted one error
record per enumerated file - tens of thousands of stderr records on
large trees, looking like a hang - and could still resolve as SUCCESS
with exit 0.

Prepend $ErrorActionPreference='Stop'; to the script content executed
by the ScriptBlock so the first error terminates the command with a
non-zero exit and a single error message. Concatenated on the same line
as the user command so error line numbers stay unshifted.

Fixes #13285

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

* fix(shared): set the fail-fast preference in the bootstrap scope

Setting $ErrorActionPreference='Stop' by string-prepending it into the
scriptblock source displaced a leading param(...) from its mandatory
first-statement position, so scripts beginning with a param block failed
with CommandNotFoundException. Preference variables are dynamically
scoped, so setting Stop in the -Command bootstrap gives the invoked
scriptblock identical fail-fast semantics while keeping the user script
byte-identical (param works, error positions unshifted) and drops the
doubled-quote escaping.

* docs(shared): document the fail-fast tradeoffs in the PowerShell wrapper

Stop promotes every non-terminating error, not only per-item pipeline
floods: partial-result commands (recursive listings over access-denied
junctions) now stop at their first error, and Windows PowerShell 5.1
turns in-script stderr redirection of succeeding native commands fatal.
State this in the wrapper comment as a deliberate tradeoff, with the
GitHub Actions precedent and the per-command opt-outs.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* ci: stop over-long changelogs from silently dropping release Slack posts (#12955)

Slack section blocks reject text longer than 3000 characters. The Slack
action logs that rejection as ##[error] but does not fail the step, so an
over-long changelog drops the release announcement while the run stays
green — cline@3.0.50 (3272 chars) published to npm, tagged, and cut a
GitHub release with no Slack post and nothing red to notice.

Every publish workflow pasted the changelog section verbatim into one
section block, so all six were exposed; the SDK, desktop, and extension
sections were only 150-350 chars under the ceiling.

Add a slack_content output alongside content: unchanged when the section
fits, otherwise trimmed on a line boundary with a link to the full
release notes. Only the Slack payload uses it — GitHub release bodies and
the desktop updater manifest still get the whole section.

* ci: tidy workflow cache config and job permissions (#13403)

Publish workflows now always do clean npm installs (no dependency
cache in their test gates), the e2e workflow's cache keys are
exact-match only, and the e2e job drops an id-token permission it
never used.

* Rename desktop app from "Cline Code" to "Cline" (#13401)

* Rename desktop app from Cline Code to Cline

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

* Format touched Rust test assertions

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

---------

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

* fix(llms): surface provider-executed tool activity as observational events (#13300)

* fix(llms): surface provider-executed tool activity as observational events

Provider-executed tool parts (e.g. every tool the Claude Code CLI runs
inside its own session) were dropped by the model-tool guard added for
web search: only declared model tools were re-emitted, everything else
hit continue with nothing yielded. Those sessions modified the workspace
with no tool activity in runtime events, transcripts, or the UI.

Route all providerExecuted parts onto the observational path instead:
emit execution-tagged tool-call-delta and tool-result events, matched by
tool-call ID for providers that omit the flag on the result half. They
stay out of AgentRuntime's execution/approval loop, and the runtime
already persists them as modelToolActivities and projects them for
display.

The AgentModelEvent tool-result variant widens toolName from
ModelToolName to string to carry the provider's own tool names.

* fix(agents): keep turns that are only provider-executed tool activity

A turn consisting solely of observational tool activity has an empty
assistant content array - the activity lives in message metadata, since
projecting it into content would replay tool_use blocks the model never
gets results for. The empty-content guard threw on such turns, erroring
the run and losing the activity from the transcript. Count model-tool
activity as content for the emptiness check (error finishes still
throw); replay stays safe through the codec's empty-content placeholder.
Also drop the trailing text delta from one gateway test so the tool-only
stream shape stays covered end to end.

* feat: allow agents to create scheduled tasks (#13331)

* feat(core, desktop): add durable todo agenda

* fix(desktop): secure todo approvals and track tool usage

* fix(desktop): clean up failed approval delivery

* fix(desktop): authenticate approval connections

* fix(desktop): cancel approvals on broadcast failure

* fix(desktop): authenticate development approvals

* fix(desktop): harden development approvals

* test(core): make task paths cross-platform

* fix(desktop): serialize approval readiness

* refactor(core): unify todo and schedule tools

* feat(core): distinguish user todos from agent suggestions

* fix(core): hide tasks tool in yolo mode

* fix(core): enforce schedule workspace scope

* fix(core): bind schedule scope to hub connection

* fix(core): establish task scope at hub startup

* fix(core): scope task automation by workspace

* test(core): normalize workspace path expectations

* test(core): serialize Windows CI workers

* fix(core): reject unregistered schedule authority

* fix(desktop): guard task execution commands

* fix(core): avoid polynomial regex in mention parsing

* fix(core): address schedule tool review feedback

* fix(core): bind websocket clients to hub workspace

* fix(core): flatten tasks tool input schema

* fix(core): authorize multi-workspace hub clients

* test(core): type hub transport authority mock

* fix(cli): register a workspace client for remote schedule commands (#13398)

---------

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

* fix(desktop): treat ClinePass as OAuth-managed in the chat credential gate (#13404)

* fix(desktop): treat ClinePass as OAuth-managed in chat credential gate

ClinePass shares the Cline account OAuth credentials (its auth handler
stores under the "cline" provider), so the webview never sees a plain
API key for it. The chat pre-flight check only exempted cline/oca/
openai-codex, so switching to ClinePass while signed in via OAuth
blocked with "Missing API key" even though the sidecar resolves the
stored access token fine (which is why the CLI worked).

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

* style: format helpers.test.ts with biome

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

---------

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

* fix(ui): stack code block lines when streamdown lineNumbers is off (#13412)

streamdown renders each Shiki token line as a bare inline span with no
newline text between non-empty lines, and only applies its block line
class when lineNumbers is on. With lineNumbers off (the desktop app's
config) every multi-line fenced block collapsed into one run-on line.
Make the direct line spans under code-block-body display: block in the
shared markdown.css; empty lines keep their height via their lone "\n"
child under white-space: pre.

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

* fix(desktop): work summary undercounts wall time when pre-tool thinking attaches to the answer (#13413)

* fix(desktop): anchor work summary duration on the answer row, not attached pre-tool reasoning

The collapsed 'Worked for Xs' row undercounted wall time whenever a turn's
assistant message contained thinking + tool_use with no narration text: the
canonical projection emitted the reasoning-only row after the tool row (both
stamped before the tool executed), the webview attached that row to the final
answer, and collapseCompletedWork used the answer's earliest attached
reasoning timestamp as the end anchor - excluding the entire tool execution
(e.g. 'Worked for 5s' for a turn with an 8s command).

- webview: end the work span at the answer row's own timestamp, clamped to
  the last collapsed row so a fallback answer bubble with a synthetic early
  timestamp cannot shrink the duration either
- sidecar: flush pending thinking before a tool_use row so rehydrated
  transcripts keep the live-stream order (thinking before its tool call) and
  pre-tool reasoning no longer rides on the next answer

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

* fix(desktop): keep interleaved thinking between the tool calls it separates

Address Greptile review: when one assistant message interleaves thinking
between multiple tool_use blocks, each reasoning segment now projects at its
own position (attached to a text row from its own segment when present,
otherwise as its own row) instead of merging into the first reasoning row,
which displayed later thinking before a tool call it actually followed.

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

---------

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

* fix(desktop): remove settings gear hover state while Account screen is open (#13408)

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

* fix(desktop): don't show "No sessions found" while session history is still loading (#13414)

* fix(desktop): don't show 'No sessions found' while session history is still loading

Replace the isLoadingHistory flag with hasLoadedHistory, set only once the
backend has actually answered a list_discovered_sessions request. The sidebar
and Sessions view now keep their loading state until that first definitive
response, so the empty-state copy can no longer appear while history is still
being fetched (or while a failed fetch is being retried).

Also retry a failed initial fetch on the 2s event cadence instead of stranding
the UI until the 12s periodic poll, which is what stretched the misleading
empty state to ~10 seconds after a webview reload when the websocket lost the
race with the page load.

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

* fix(desktop): stop history fast-retry from re-arming after hook unmount

A failed initial fetch that settles after the hook unmounted could schedule a
new retry timer after cleanup had already cleared the refs, leaving the
abandoned hook polling the backend every 2s. Guard scheduleRefresh with a
disposed ref set by the mount effect's cleanup.

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

---------

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

* Fix @ file mentions breaking on paths with spaces (#13391)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

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

* Fix import ordering in mentions test (biome organize imports)

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

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

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

* Normalize mention paths to posix separators for Windows

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

---------

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

* Fix code actions failing with "command not found" on VS Code 1.134 (#13402)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

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

* Fix import ordering in mentions test (biome organize imports)

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

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

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

* Normalize mention paths to posix separators for Windows

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

* Fix code actions failing with 'command not found' on VS Code 1.134

Code action commands carried arguments (expandedRange, diagnostics),
which routes them through VS Code's CommandsConverter cache. VS Code
1.134 disposes the cached entries before the clicked action executes,
so every lightbulb action failed with 'Actual command not found,
wanted to execute cline.addToChat'.

Drop the arguments so the command id is passed through directly, and
recover the context in the handler instead: getContextForCommand now
expands an empty selection by 3 surrounding lines (matching the old
provider behavior) and gathers document diagnostics intersecting the
range when none are passed explicitly.

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

* Scope gathered diagnostics to the selection/cursor

Match the old CodeActionContext.diagnostics behavior: only include
diagnostics intersecting the range the action was requested for, not
the surrounding lines the text gets expanded to.

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

---------

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

* Desktop: unify Plugins, MCP, and Skills into one Plugins hub with a dedicated Marketplace page (#13411)

* Unify desktop plugins, apps, MCP, and skills into one Plugins hub with a Browse directory mode

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

* Open the marketplace directory as a modal over the Plugins hub instead of swapping the page

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

* Rename directory to Marketplace: Browse Marketplace button, Marketplace modal title with icon, search placeholder

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

* Fix search input focus ring clipped by the Marketplace modal scroll container

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

* Address Greptile review: keep selected tag chip visible when its count drops to zero, and remount installed tab when a marketplace install completes after the modal closed

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

* Track marketplace modal mutation flag in a ref so a close click racing a queued render cannot skip the inventory remount

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

* Make Marketplace its own settings page under Customizations and restore Channels as a standalone page

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

* Remove icon from Marketplace page header for consistency with other settings pages

* Notify mounted inventory views when the marketplace invalidates the cache so late install completions refresh the Plugins hub

---------

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

* feat(desktop/ui): recommended and free model tiers in the composer model selector (#13410)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

---------

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

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK (#13415)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

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

* feat(desktop): recommended-feed badges and descriptions in provider settings (#13416)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* feat(desktop): recommended-feed badges and descriptions in provider settings

Review suggestion on #13410: the provider settings page has room for
more model detail than the composer's picker. The cline/cline-pass
provider cards now refresh their model list through
list_provider_models (the catalog snapshot deliberately skips the
recommended-feed overlay so the startup catalog fetch never blocks on
the feed) and render Recommended/Free tier badges plus feed tags (NEW)
next to the model name, with the model description underneath. The
refreshed list also surfaces the live entries instead of the bundled
snapshot.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

* fix(desktop): scope the settings featured model list to its provider and revision

The fetched featured list was unscoped component state: switching
between cline and cline-pass reused the component instance, so the
previous provider's models stayed visible while the new request was
pending (or forever, when it failed), and the retained copy shadowed
later provider.modelList updates — adding a second custom model
submitted the stale list as the complete configuration and dropped the
first addition.

The fetched list now only applies to the provider and modelList
revision it was fetched for (falling back to the catalog snapshot
otherwise and refetching on membership changes), and add-model submits
the union of the displayed and configured ids so an update can never
silently unconfigure existing entries.

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

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

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

* fix(ui): update packed-Tailwind smoke contract for the picker's max-h-64 (#13421)

The ui-publish smoke check pins a set of Tailwind candidates the packed
sources must emit; #13410 grew the SearchCombobox options list from
max-h-56 to max-h-64, so the publish run failed on the stale candidate.
All other pinned candidates verified against the current sources.

* feat(desktop): refresh app icons and branding (#13400)

* ci(vscode): upload E2E failure recordings from the right path (#13427)

The job sets working-directory: apps/vscode, but that default applies to run
steps only, not to `uses:` steps. Since #10961 moved the extension under apps/
and added that default, the artifact path has resolved against the repo root,
matched nothing, and every failing run logged "No files were found with the
provided path: test-results/playwright/" instead of uploading recordings.

Widen to test-results/ so Playwright's error-context snapshots ship alongside
the videos.

* fix(hooks): deliver tool hook contextModification to the model (#13297)

* fix(hooks): deliver tool hook contextModification to the model

On the next engine, a tool_call (PreToolUse) hook's contextModification
was parsed into HookControl.context and then silently dropped: the
runtime beforeTool/afterTool result contract had no channel for
injecting conversation context. Legacy consumed it (ToolExecutor /
ToolHookUtils pushed <hook_context> blocks into the next user turn), so
this was a regression of documented behavior.

- Add appendContext to AgentBeforeToolResult/AgentAfterToolResult.
- AgentRuntime collects appendContext across hooks during an
  iteration's tool executions and appends one <hook_context> user
  message after the tool results, keeping tool-result parts contiguous.
- Map HookControl.context into appendContext in both subprocess hook
  layers (skipped when the hook cancels, matching legacy, where the
  message doubled as the error).
- Truncate injected context at 50KB per hook output, matching legacy.
- Concatenate appendContext across merged hook layers.

tool_result (PostToolUse) hooks still run detached with stdout ignored;
making them blocking so their context can be collected is a follow-up.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): stamp tool identity on injected hook context blocks

Contexts are batched into one message after the tool results, and
parallel tool execution collects them in completion order, so position
alone cannot attribute a block to its tool call. Add tool_name and
tool_call_id attributes to each <hook_context> block.

* fix(hooks): sanitize hook context block markup

Attribute values (tool_name, tool_call_id) are stripped of quote/angle
characters and embedded </hook_context> closers in hook output are
neutralized, so neither provider-supplied ids nor hook text can corrupt
or spoof a block's stamped identity.

* fix(hooks): neutralize forged opening hook_context tags in hook output

The previous sanitization only neutralized closing tags, so hook output
could still open a forged <hook_context> block claiming another tool's
identity. Escape both opening and closing embedded tags with one rule.

* fix(hooks): hide injected hook context from user-facing transcripts

Stamp the injected hook-context user message with displayRole 'system'
(the compaction-summary convention) so it reaches the model but does
not render as a user bubble in live or replayed transcripts. Without
this, resuming a session showed the raw <hook_context> block as if the
user had typed it.

* fix(hooks): neutralize case-variant embedded hook_context tags

The tag-neutralization regex was case-sensitive, so hook output could
still smuggle a forged tag as <HOOK_CONTEXT>. Match case-insensitively.

* fix(vscode): map PreToolUse contextModification into runtime appendContext

The extension's hooks adapter bridged file hooks into the SDK runtime
but forwarded only cancel/errorMessage, so a PreToolUse hook's
contextModification never reached the model. Map it into the runtime's
appendContext channel; HookFactory already truncates it at 50KB.

* fix(vscode): hide hook-injected context from replayed transcripts

Live sessions never rendered the injected <hook_context> user message,
but session reload replayed it as a user bubble (and post-resume turns
kept doing so). Treat these messages as synthetic in the user-message
mapping: honor the displayRole 'system' stamp the runtime sets, with a
text-prefix guard for paths where metadata is unavailable. This also
keeps edit/regenerate ordinal mapping aligned with visible bubbles.

* fix(hooks): run file hooks through exactly one layer per host

The VS Code extension registered two independent hook execution layers:
its own hooks adapter (config.hooks) and the SDK core's file-hook
extension from the runtime bootstrap. When both discover the same hook
files, every hook executes twice per event — and with context injection
wired, each contextModification would be injected twice.

Add a 'hooks' runtime config extension kind (in the default set, so the
CLI keeps core file hooks unchanged) and gate the bootstrap's file-hook
extension on it. The extension excludes 'hooks' at session start, so
its adapter — which also provides the hook status UI and the
hooksEnabled setting — is its single execution path.

* fix(vscode): discover hooks from the session workspace, not only global state

Hook discovery read workspaceRoots from global state shared across
every Cline instance, so another window repointing it made workspace
hooks silently stop being discovered. With the extension's adapter now
the single hook execution layer, that meant no hooks at all.

HookFactory takes an optional sessionWorkspaceRoot and unions that
root's .clinerules/hooks into discovery (and into cwd resolution), fed
from the session config's cwd. Shared-state discovery still works, so
behavior in the single-window case is unchanged.

* fix(hooks): keep sanitized hook attribute values distinguishable

Replacing every markup delimiter with the same underscore could
collapse two tool call ids that differ only by such a character into
identical stamps. Escape each delimiter with a distinct token instead.

* fix(hooks): make hook attribute sanitization injective

Escaping the underscore itself turns the attribute escaping into a
uniquely decodable code, so no two distinct tool call ids can collapse
to the same sanitized stamp (previously an id containing a literal
escape token could collide with an id containing the delimiter).

* fix(vscode): reconstruct hook status rows when replaying transcripts

hook_status messages are emitted live but never persisted, so reloading
a session dropped every hook row. The injected <hook_context> blocks
carry the hook source and tool name, so the replay translator now
rebuilds a completed hook status row from each block. The injection is
also no longer treated as a user turn boundary, so the final turn's
completion retag is unaffected by it.

* fix(hooks): collect PostToolUse hook output and honor its control (#13298)

* fix(hooks): collect PostToolUse hook output and honor its control

tool_result (PostToolUse) hooks ran fire-and-forget with stdout
ignored, so their entire JSON output — contextModification and cancel —
was discarded. Legacy awaited PostToolUse, injected its
contextModification into the conversation, and honored cancel.

- Run tool_result hook commands blocking (same 120s default timeout as
  tool_call) in both the hook-config-file layer and the agent-hook
  subprocess layer.
- Map their output: cancel stops the run with the hook's error message
  as the reason; otherwise context is injected via afterTool
  appendContext.

This restores legacy blocking semantics: tool results now wait for
tool_result hooks, but only in sessions that have one configured.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): bound tool_result hook wait and isolate cancel reason

Address review findings:
- The agent-hook subprocess layer forwarded an unset timeoutMs
  unchanged, so a tool hook command that never exits would block the
  agent indefinitely. Default both tool_call and tool_result to the
  120s bound the hook-config-file layer already used.
- A cancelling hook's error message was folded into the same context
  field as other hooks' injectable context, so merging controls could
  leak unrelated hook context into the cancellation reason. Carry it as
  a separate cancelReason, and surface it as the stop reason for
  beforeTool cancels too.

* fix(hooks): prefer errorMessage as a cancelling hook's stop reason

When a cancelling hook returns both contextModification and
errorMessage, the context-first parse precedence made the injectable
context the cancel reason and discarded the actual error. Parse the two
fields separately: errorMessage wins as the cancel reason (matching
legacy), and a lone errorMessage still folds into injectable context
for non-cancelling hooks as before.

* fix(vscode): honor PostToolUse hook cancel and contextModification

The adapter awaited PostToolUse hooks but discarded their output
entirely. Map cancel to a stop control (with errorMessage as the
reason) and contextModification into the runtime appendContext channel,
matching the PreToolUse mapping and legacy semantics.

* fix(hooks): whitespace-only errorMessage no longer suppresses the cancel reason

A cancelling hook returning meaningful context alongside a blank
errorMessage lost both: the parsers selected the whitespace as the
reason and the result mappers trimmed it away. Require a non-blank
errorMessage before it wins, so context serves as the fallback reason.
Apply the same fallback in the extension adapter's stop mapping.

* fix(core): stop Windows CI worker crashes from the agenda spec watcher (#13428)

* fix(core): watch agenda task specs via the resolved long path

fs.watch on a path with 8.3 short components (e.g. C:\Users\RUNNER~1
temp dirs) trips a libuv assertion in fs-event.c on Windows and aborts
the whole process. Since the agenda task manager landed, every hub
server test spins up its spec watcher on such a path on hosted Windows
runners, killing the vitest worker and failing the sdk-test Windows job
on every branch. Resolve the specs dir with realpathSync.native before
watching so libuv only ever sees the long form.

* test(ui): stub ResizeObserver for @pierre/diffs in tool-diff tests

jsdom does not implement ResizeObserver, so every ToolFileDiff render
logged a ReferenceError from @pierre/diffs to stderr. Tests still
passed; this just silences the noise the same way the constructable
stylesheet shim does.

* fix(core): skip the agenda spec watcher when the dir does not resolve

Falling back to the raw path on realpath failure would reintroduce the
Windows short-path abort; log and go without the watcher instead.

* fix(vscode): honor the classic truncation range when migrating legacy tasks (#13419)

Classic Cline truncated long conversations by omitting an index range of
api_conversation_history from every API request (keep the first
user-assistant pair, drop everything through the range end, strip
orphaned tool_results from the first kept message). The range was
persisted on the history item while the full history stayed on disk.

legacyApiHistoryToSdkMessages ignored conversationHistoryDeletedRange
and converted the entire file, so resuming a migrated long task handed
the SDK an untruncated working context that could exceed the model's
context window by millions of tokens - every request failed with
'prompt is too long' and every compaction restarted from the full
history (#12996, confirmed by the reporter: the task was migrated from
an older version and broke after a restart, with each compaction
starting from ~3M tokens).

The migration now replays exactly what the classic extension sent:
slice out the deleted range and drop orphaned tool_results, mirroring
ContextManager.getTruncatedMessages (see origin/main). Malformed ranges
fall back to the full history (previous behavior).

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

* fix(vscode): show the diff edit view for multi-line edits in CRLF files (#13417)

The edit preview computed proposed content with an exact old_text match, but
the SDK executor normalizes old/new text to the file's own line endings before
matching (#12305) - reads strip CR, so models emit LF-only text even for CRLF
files. Any multi-line old_text in a CRLF file therefore failed the preview's
match: the diff edit view silently never opened while the executor applied the
edit. Single-line edits (no line break in old_text) were unaffected, which is
why the diff view appeared to trigger inconsistently.

Mirror the executor's EOL normalization (and its literal $-sequence insertion)
in the preview computation.

Fixes #13296

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

* fix(core): report truthful session status so desktop checkpoint restore stops wedging (#13418)

* fix(core): keep hub session status truthful across queue-drained turns

Queue-drained turns settle only through the event stream, but the hub
runtime host mistranslated their lifecycle in two ways:

- session.updated events carrying only a snapshot (persistence updates)
  defaulted the projected status to "running". When one trailed the
  final idle update after a turn, clients that track busy state from
  status events (the desktop sidecar's workspace restore gate) stayed
  busy forever. Use the snapshot's real status and emit nothing when
  neither source reports one.
- the per-run agent.done dedup was only reset by run.started, which the
  daemon-side queue drain never publishes, so a drained turn's done was
  swallowed as a duplicate of the previous turn's. Reset the dedup on
  session.pending_prompt_submitted, and suppress stale run.completed
  events that land inside a drained turn's window so they can neither
  emit a phantom done nor consume the drained turn's dedup slot.

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

* test(desktop): cover restore unlock after an event-settled queued turn

Exports the sidecar's core-session event handler so the queued-turn
lifecycle (busy via status events, cleared by the done agent event,
restore allowed afterwards) is testable end-to-end.

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

* fix(core): start interactive sessions without a prompt as idle

The runtime host reported every new session as "running" until its
first turn ended. Interactive hosts (the desktop app) start sessions
with no prompt and dispatch turns through separate send calls, so a
created-but-never-prompted session stayed "running" forever — wedging
clients that gate workspace operations (checkpoint restore, message
edit) on active turns.

Interactive no-prompt starts now begin idle, start emits the session's
actual status (resumed sessions no longer masquerade as running), and
markTurn* transitions keep tracking in-memory status for lazily
persisted sessions so the first turn still reports running -> idle.

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

* style: format hub-runtime-host test filter

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

* refactor: drop the drained-turn done bookkeeping, keep the minimal fix

The stuck restore is fully explained by the two status defects (fabricated
"running" from snapshot-only session.updated events, and never-prompted
interactive sessions reporting "running"). The done-dedup machinery for
queue-drained turns addressed a separate cosmetic gap (queued turns emit no
chat_done, pre-existing) and required fragile run-window heuristics, so it
is removed to keep this change reviewable. Sidecar test now settles the
queued turn through the status event, matching the shipped mechanism.

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

* docs(sdk): document the truthful session-status contract

---------

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

* chore(deps): update Langfuse packages and bump app versions (#13443)

* chore(deps): update Langfuse packages and bump app versions

Update @langfuse/otel to v5.10.1 and add @langfuse/vercel-ai-sdk v5.9.1 for improved observability with Vercel AI SDK.

Bump versions for @cline/code to 0.0.14 and @cline/ui to 0.2.0-next.6, updated via bun.lock.

Other Changes:
Added optional userId to AgentRuntimeConfig.
Propagated userId, sessionId, conversationId, runId, iteration, provider, and model context into AI SDK telemetry.
Added AI SDK 7 runtimeContext with explicit includeRuntimeContext.
Added stable OTEL_SERVICE_NAME=cline-sdk.
Added runtime metadata assertions in agent tests.

* add taskId

* Revert "add taskId"

This reverts commit f20d31d96d.

* docs: simplify Open Cline step in installing guide (#13405)

* docs: remove duplicate GLM-5.3 rows in ClinePass tables (#13449)

Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>

* chore(sdk): release v0.0.76

* chore(cli): release v3.0.56

* docs(cli): scope the v3.0.56 release notes to CLI-visible changes

* feat(desktop): interactive welcome hero graphic (#13399)

* feat(desktop): add interactive welcome hero

* feat(desktop): support composable welcome hero variants

* feat(desktop): reskin first-run onboarding (#13441)

* refactor: centralize client tool availability (#13451)

* chore(sdk): release v0.0.77

* docs(cli): drop the tasks tool from the v3.0.56 notes, it is desktop-only

* chore(vscode): prepare 4.1.11 release

* chore(desktop): release v0.0.15

* fix(vscode): remote config MCP settings (#13466)

* fix(vscode): enforce enterprise MCP controls on the Customize marketplace

The unified Customize marketplace replaced the old MCP marketplace
without carrying over enterprise remote-config enforcement: the catalog
RPC returned every MCP entry and installs were never policy-checked,
so orgs with mcpMarketplaceEnabled=false or an allowedMCPServers
allowlist saw (and could install) all marketplace MCP servers.

- Filter MCP entries out of getMarketplaceCatalog when the marketplace
  is disabled, and restrict entries to the allowlist when configured
  (matching entry id, display name, installed server name, or source
  repo URL, mirroring legacy GitHub-URL allowlist ids)
- Reject installMarketplaceEntry requests that violate the policy
- Map the published catalog's repo/homepage fields onto
  sourceUrl/homepageUrl so URL-based allowlists can match
- Update the enterprise MCP server controls docs

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

* refactor: simplify MCP marketplace policy enforcement

Fold the policy check into marketplace-helpers, drop the dedicated
test suite, and trim the docs edit to the strictly necessary line.

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

---------

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

* Treat an empty preserved capability list as unspecified when seeding tools (#13465)

* Treat an empty preserved capability list as unspecified when seeding tools

toSdkModelInfo guarded the tools seeding with a strict
preservedCapabilities === undefined check, but modelHasCapability —
the runtime's own reader — treats undefined AND length === 0 as
"unspecified". A custom OpenAI-Compatible model whose stored
capabilities field is a defined-but-empty array (a config carried over
from before the field existed, or one round-tripped through a boundary
that defaults it to []) skipped the seeding; the first boolean
projection to run afterwards (e.g. supportsReasoning) then populated
the array, the runtime gate read the non-empty, tool-less list as
authoritative, and every tool definition was silently dropped from the
session (#13463).

The guard now covers the empty array too, matching the reader's
unspecified semantics.

* test: satisfy the store's isModelInfo gate so the empty-capabilities case actually reaches knownModels

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

---------

Co-authored-by: yzxcj797 <yzxcj797@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>

* chore(vscode): prepare 4.1.12 release

* Add feature flags to the desktop app (#13289)

* Add feature flags to the app

* React to account updates

* Address comments

* use a per-app file

* fix: propagate Langfuse session telemetry (#13473)

* fix telemetry session propagation

* feat telemetry client version metadata

* fix(core): address Langfuse review feedback — hub client identity + delegated agent session grouping (#13475)

* fix(core): rebuild hub session client identity from request headers

Hub-backed sessions do not transport extensionContext (it is local-only),
so the daemon's runtime built traces without the clientName/clientVersion
metadata even though the hub client bakes X-CLIENT-TYPE / X-CLIENT-VERSION
into the session's provider headers. Reconstruct extensionContext.client
from those headers during local runtime bootstrap so hub-backed Langfuse
traces carry the same client identity as local runtimes, and the daemon's
header re-resolution stops clobbering the original X-CLIENT-TYPE.

* fix(core): propagate parent distinctId/sessionId to delegated agents

Delegated agents (spawned sub-agents, configured agents, teammates) were
built without distinctId and sessionId, so their Langfuse traces had no
userId or sessionId and did not group with the parent user or session.
Thread the host-resolved distinctId through RuntimeBuilderInput and the
root sessionId through the delegated-agent config provider, and copy both
onto the delegated AgentConfig.

---------

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

---------

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

* ci(vscode): make combined nightly manual-dispatch only

The PublishNightly environment gained required reviewers, so each cron
run parked on approval, held the workflow's concurrency group, and
silently cancelled every scheduled run queued behind it. 20 consecutive
scheduled nightlies died this way between 2026-07-31 and 2026-08-21;
the only nightlies that shipped in that window were manual dispatches.

Drop the cron rather than leave a trigger that cannot succeed unattended.

* feat(hub): add drain and upgrade commands with replay support (#13468)

* feat(hub): add drain and upgrade commands with replay support

* handles disconnection

* feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport

Completes the wiring the previous commits' primitives needed:
HubServerTransport gains isDraining(), hub.drain/hub.status/profile.get
command handling, and replayEventsAfter() (backed by the durable event
log), plus the sequence/sinceSequence wire types they depend on in
shared/hub.ts. run-queue-handlers.ts reads the active bot profile's
plugin roots when executing durable runs.

Also adds hub/profiles/: profile.json (identity/rules/plugins) ->
system prompt composition, --profile / CLINE_HUB_BOT_PROFILE
resolution, and the bundled cline-dad profile with its
cline_hub_support read-only diagnostics tool.

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

* Revert "feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport"

This reverts commit 6696d5d202.

* fix(hub): dedupe replayed events by eventId, not just sequence

HubEventLogStore.append() returns a new envelope stamped with a
sequence rather than mutating the input, so a pending approval
re-issued sequence-less by subscribe() (it predates any durable-log
append) and its later sequence-stamped copy from the durable log are
two different objects carrying the same eventId. The replay-then-live
buffer in browser-websocket.ts only deduped by sequence, so the
sequence-less copy's guard never tripped and it was delivered a second
time when the buffer flushed after replay.

Track delivered eventIds alongside the sequence cursor; eventId
survives the append/stamp round-trip unchanged, so this dedupes the
exact-same logical event regardless of which copy arrives first.

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

* fix(hub): wire drain, durable event log, and run queue into the live transport

CI on this branch failed bun run build:sdk: browser-websocket.ts,
client/index.ts, and hub-websocket-server.ts (already on this branch)
reference sequence/sinceSequence, HubServerTransport.isDraining(), and
the "hub.drain" command — but the commit that reverted bot profiles
out of this branch also reverted this wiring, since it shared a commit
with the profiles work. That wiring is a hub concern, not a
bot-profiles one; split it back out.

- shared/hub.ts: sequence/sinceSequence types, run.enqueue/run.list/
  hub.drain/hub.status/stream.replay capability, command, and event
  names. profile.get intentionally excluded — stays bot-profiles-only.
- context.ts: isDraining() on HubTransportContext. botProfile field
  intentionally excluded.
- hub-server-transport.ts: eventLog/runQueue fields and start/stop
  lifecycle, publish() appends to the durable log, handleCommand cases
  for run.enqueue/run.list/hub.drain/hub.status, drain-refusal check,
  replayEventsAfter()/lastEventSequence(). startBotProfile()/
  startHubSupportTool() and the profile.get case intentionally
  excluded.
- run-queue-handlers.ts: added without handleProfileGet (needs
  ctx.botProfile, which doesn't exist here).
- hub-upgrades.test.ts: added without its two bot-profile-injection
  tests (they need a resolved bot profile to assert against).

Verified bun run build:sdk exits 0 (the exact CI command) and
bunx vitest run src/hub passes (311/312; the one failure is the
same pre-existing environment-timing flake already present before
this change).

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

* fix(hub): export instance-lock, event-log, and run-queue from the hub barrel

These landed as internal modules only; hub-server-transport.ts and
hub-websocket-server.ts import them by direct path, but nothing
re-exported them from the public @cline/core/hub surface the way
sibling discovery/server modules already are.

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

* fix(hub): wire the instance lock into the daemon entry point

The singleton lock (discovery/instance-lock.ts) and its consumption in
startHubWebSocketServer/ensureHubWebSocketServer were already on this
branch, but the daemon entry point's own half was not: retrying a bind
when a retiring predecessor still holds the lock, and exiting with a
distinct code (3) instead of the generic fatal path when a live Hub
already owns the data directory. Without this, a daemon racing a
retiring predecessor could fail outright instead of waiting the lock
out, and losing the singleton race looked identical to a crash.

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

* fix(hub): address drain/upgrade review findings (#13478)

- cline hub upgrade: check idleness at least once (--wait 0 works), reject
  non-numeric --wait, and un-drain on every abort path so an aborted
  upgrade can never leave the hub refusing new work
- add cline hub drain --off and the off query param to requestHubDrain so
  POST /drain?off is reachable from shipped code
- HubEventLogStore/HubRunQueue: WAL journal mode + busy_timeout, and stamp
  sequences from lastInsertRowid instead of SELECT MAX(sequence)
- HubInstanceLock.acquire: degrade to an unheld lock when SQLite is
  unavailable instead of refusing hub startup; only BUSY/LOCKED still
  raises HubLockHeldError
- ensureHubWebSocketServer: retire an unusable discovered hub through the
  shared retireDiscoveredHub (busy hubs are attached to, drain precedes
  shutdown, discovery cleared only when the hub actually retired)
- replay adapter: advance the cursor past eventId-deduped events, cap
  replay pages, stop when the cursor stalls, and drop the dedupe set after
  the buffered flush so it cannot grow for the socket lifetime

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

* fix(hub): derive the singleton e2e challenger cwd portably

The challenger's working directory was derived by round-tripping the
discovery path through a file: URL and stripping the last pathname
segment. On Windows that yields a POSIX-style '/C:/...' path, which is
not a valid spawn cwd, so the spawn fails ENOENT before the singleton
lock is ever contested and the Windows SDK test job goes red.

The data dir is simply the discovery file's parent: use dirname().

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop stored capability lists from silently revoking tool calling for custom models (#13476)

* fix(core): seed tools capability when custom model capabilities are synthesized from boolean flags

For a models.json entry with no explicit capabilities list, toStoredModelInfo
synthesized a capability array purely from boolean convenience flags (e.g.
supportsReasoning: true -> ["reasoning"]). modelSupportsToolCalling fails open
only for a missing or empty list, so the synthesized non-empty list read as an
authoritative denial and silently stripped every tool definition from requests
to custom OpenAI-compatible models (#13463).

Seed "tools" whenever the list was not explicitly authored and the boolean
projections made it non-empty, preserving the fail-open contract. Explicitly
authored capability lists remain authoritative and can still disable tools.

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

* test(core): cover stale catalog capability overrides

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

* fix: treat stored capability lists as non-authoritative for tool calling

The hasExplicitCapabilities guard still let two producers of tool-less
lists through:

- The VS Code legacy-override migration (legacyModelInfoToOverrides)
  persists explicit partial lists like ["prompt-cache"] into models.json
  for custom OpenAI-compatible models, which then read as an authoritative
  "cannot call tools" and drop every tool - same symptom as #13463.
- Any hand- or UI-authored partial list on a non-catalog model.

Stored entries and user-authored provider metadata have no way to declare
"cannot call tools" (there is no supportsTools field, and every writer
that authors a full list includes "tools"), so seed "tools" into any
non-empty list for a language model. Only generated catalog capabilities
remain authoritative - a genuine no-tools catalog model stays that way -
and non-language models (e.g. image generation) never gain a tools claim.

Also make legacyModelInfoToOverrides write "tools" into the arrays it
fabricates, matching the providers.json migration, so models.json stops
being poisoned for older readers.

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

---------

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

* chore(vscode): prepare 4.1.13 release

* chore(sdk): release v0.0.78

* chore(cli): release v3.0.57

* fix(core): run hub e2e files serially so daemon timing budgets survive CI contention

singleton.e2e.test.ts (added in #13468) spawns real daemons and runs for
~15s. Vitest's default file parallelism let it run alongside
shutdown.e2e.test.ts, whose assertions are wall-clock bound: discovery
within 10s, exit within 5s, and a 2s shutdown watchdog. On the 2-core
windows-latest runner that contention alone broke those budgets, failing
the shutdown test two different ways across runs — once never observing
discovery, once with the daemon forced to exit before its HTTP 202
flushed (socket hang up). The test passed on Windows before #13468 and
has failed every SDK publish run since.

* chore(desktop): release v0.0.16

* test(sdk): give windows-sensitive suites realistic timeouts

Four consecutive SDK publish runs failed on windows-latest, each on a
different test, all of them plain timeouts: two @cline/shared SQLite
tests at the 5s vitest default, core's bash executor at 10s, and the hub
singleton endpoint test at 10s. The 2-core Windows runner spawns forks
and takes SQLite locks slowly enough to blow those budgets under load.

These timeouts guard against hangs; they are not timing assertions (the
one suite that does assert elapsed time, shutdown.e2e, was fixed by
removing file-level parallelism instead). Raise core to 20s and give
@cline/shared an explicit 15s in place of the inherited 5s default.

* fix(telemetry): emit task.completed from every session teardown path (#13489)

The task.completed fallback lived only inside shutdownSession, but
stopSession/dispose route interactive sessions with a terminal reported
status through releaseSessionRuntime, which never emitted. Truthful
session-status reporting (shipped in 4.1.11) re-routed a large share of
interactive stops onto that branch and silently dropped the event.

Route the emission through a single choke point,
emitTaskCompletedOnTeardown, called from both shutdownSession and
releaseSessionRuntime. The completion criterion no longer reads
session.status: interactive sessions use the recorded final-turn
outcome (lastInteractiveTurnFinishReason), non-interactive sessions
keep the existing input.status === "completed" logic. A new
taskCompletedEmitted flag (also set by the submit_and_exit observer)
enforces exactly one task.completed per session. failSession now
records the errored final turn so a stale "completed" from an earlier
turn can never leak into the teardown emission. Telemetry only; no
user-facing behavior changes.

* chore(vscode): release v4.1.14

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on (#13498)

* fix(vscode): honor MCP auto-approve settings for SDK tool calls

The SDK extension required both the global 'Use MCP servers' auto-approve
toggle AND each tool's per-tool autoApprove flag before silently approving
an MCP call, while the legacy extension treated them as either/or. Restore
the legacy OR semantics so toggling MCP auto-approve works again.

Also key toolPolicies by the registered SDK tool name (via
defaultMcpToolNameTransform, now exported from @cline/core) instead of raw
server__tool. Servers whose names contain sanitized characters (e.g.
marketplace names like github.com/user/repo) or exceed 64 chars produced
policy keys that never matched the registered tool, so those MCP tools ran
without any approval gate; the live auto-approve lookup now re-applies the
transform instead of string-splitting the name.

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

* Revert "fix(vscode): honor MCP auto-approve settings for SDK tool calls"

This reverts commit 86c568fbba.

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on

The SDK extension only auto-approved an MCP call when the global 'Use MCP
servers' auto-approve toggle AND that tool's per-tool autoApprove flag were
both set, so toggling MCP auto-approve appeared to do nothing and users had
to opt in each tool individually. The toggle alone now governs all MCP
tools; the per-tool flag is no longer consulted.

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

---------

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

* chore(vscode): release v4.1.15

* fix(cli): remove the $4.99 ClinePass promo copy (#13514)

The $4.99 first-month promo is ending, so the CLI's first-launch "Try ClinePass" dialog should no longer advertise it. Also drops the leftover CLI_PROMO_CODE plumbing, which has been an empty string since the promo-code flow was removed.

* fix(vscode): resolve hook workspace identity from the window, not shared global state (#13352)

* fix(vscode): resolve hook workspace identity from the window, not shared global state

Hook discovery, hook cwd selection, and the workspaceRoots metadata passed
to hook scripts all read the workspaceRoots/primaryRootIndex global state
keys. Global state lives in ~/.cline and is shared by every Cline instance
(all VS Code windows, the CLI, the JetBrains plugin), and nothing writes
these keys anymore, so hooks resolved against whatever project some other
or older instance last recorded. With a second window open on another
project, a workspace's .clinerules/hooks scripts were never discovered.

Resolve workspace roots via a single guarded helper backed by
HostProvider.workspace.getWorkspacePaths() (in-process, window-scoped,
same as refreshHooks): blank paths are filtered, a host-bridge failure
degrades to no workspace roots instead of silently disabling global hooks
or skipping blocking PreToolUse guards, and one resolution is threaded
through hooks-dir discovery, cache misses, cwd selection, and hook input
metadata so they can't disagree (previously up to four host lookups per
hook execution — real gRPC round trips in the standalone host). Roots and
hooks dirs are matched on whole path segments with the longest root
winning, so prefix-sharing or nested workspace roots resolve to the right
project. The adapter creates the runner once per event and skips no-op
runners, making creation the single resolution point; the separate
hasHook/getHookInfo checks are removed. The dead workspaceRoots and
primaryRootIndex state keys are dropped, and the four hand-rolled
HostProvider.workspace test stubs are consolidated into one shared
helper.

* test(vscode): add e2e coverage for workspace-scoped hook discovery

Boots real VS Code with the packaged extension against the workspace
fixture, sends a prompt, and asserts the fixture's UserPromptSubmit hook
was discovered from the open window's workspace, executed with that
workspace root as its cwd, and received the same root in its
workspaceRoots input — the end-to-end contract the hook workspace
identity fix establishes.

* test(vscode): isolate the e2e hook fixture from the shared workspace

The UserPromptSubmit fixture hook lived in the shared e2e workspace, so
every prompt-sending spec executed it (hooksEnabled defaults to true) —
and its cold PowerShell spawn on Windows pushed chat.test.ts past the
5s expect timeout. hooks.test.ts now overrides workspaceDir to a
dedicated workspace-hooks fixture, so only the hooks spec pays the hook
spawn.

* fix(hub): cap hub-events db size so it can't fill the disk (#13516)

* fix(hub): cap hub-events db size so it can't fill the disk

Row/time retention alone didn't bound disk usage: envelopes carrying
full session snapshots reach hundreds of KB each, so retained rows
could total tens of GB, sweeps only ran hourly, and DELETE never
shrinks a SQLite file. Enforce a 64 MiB size budget in prune() (oldest
rows first, VACUUM to return the space), and also prune after every
16 MiB appended so bursts can't outrun the hourly timer.

Fixes #13505

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

* fix(hub): tolerate VACUUM failure on a full disk

VACUUM needs scratch space and can fail in exactly the state a
ballooned event log causes. The byte-budget deletes already bound live
data, so swallow the error and let the next sweep retry the reclaim
instead of aborting startup pruning and disabling the durable log.

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

* fix(hub): count the size budget in UTF-8 bytes, not characters

envelopeJson.length (UTF-16 units) and SQLite LENGTH() (characters)
undercount multibyte text by up to 3x, which could leave a CJK-heavy
log settled above budget and re-running VACUUM every sweep. Use
Buffer.byteLength and LENGTH(CAST(... AS BLOB)) instead.

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

---------

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

* fix(sdk): carry root overrides into the Node smoke-test sandbox (#13517)

ci-node-smoke.ts installs the packed SDK tarballs with a plain npm
install in a fresh temp dir, where the repo root package.json overrides
do not apply. When @sap-cloud-sdk 4.9.0 shipped (2026-08-24) it broke
@sap-ai-sdk/ai-api 2.14.0 (via @jerome-benoit/sap-ai-provider in
@cline/llms) with ERR_PACKAGE_PATH_NOT_EXPORTED, failing the smoke step
on every PR even though the root already pins @sap-cloud-sdk/* to 4.6.0.

Copy the root overrides block into the generated sandbox package.json
so the smoke install resolves the same pinned versions as the repo and
future third-party releases cannot break it independently.

* chore(sdk): release v0.0.79

* fix(vscode): don't steal last-used provider from ClinePass on credential refresh (#13520)

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

* fix(hub): flush the /shutdown 202 before daemon teardown

The /shutdown handler queued teardown on a microtask, which runs before
the event loop's write phase, so the daemon could process.exit() before
the accepted 202 was handed to the socket. Unix masked it (uv_try_write
lands small loopback writes synchronously); Windows has no such fast
path and lost the race regularly — the recurring shutdown.e2e.test.ts
'socket hang up' failures on windows-latest. Start teardown from the
response's write callback instead, with an idempotent 1s fallback so a
client that vanishes mid-write cannot strand the daemon, and send
Connection: close so the client gets a FIN rather than an abort.

Since the flakiness this compensated for is fixed at the source, restore
maxWorkers: 2 for the Windows core suite (serializing it cost ~3 min of
CI per run), and raise the e2e daemon discovery hang guard 10s→30s —
it guards against hangs, not runner speed.

* chore(cli): release v3.0.58

* fix(core): prevent search_codebase from crashing the process on giant single-line files (#13525)

* fix(core): prevent search_codebase from crashing the process on giant single-line files

searchWithRipgrep buffered all of rg's --json stdout into one string. Each
JSON event embeds the full text of the matched line (--max-columns is
ignored in JSON mode), so searching a directory of serialized trace dumps
(single-line multi-hundred-MB JSON files) accumulated gigabytes of stdout
until string concatenation threw RangeError: Out of memory inside the
stream data handler. That throw is outside the tool's try/catch, so it
escalated to an uncaughtException and killed the CLI/hub daemon.

Parse rg's JSON events incrementally line by line, drop events larger
than 256KB, truncate matched/context lines to MAX_LINE_CHARS, and stop
reading once maxResults is reached. The fallback regex scan now skips
files larger than 10MB (reporting the skip count) and truncates its
context lines the same way.

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

* simplify search_codebase crash fix to a minimal diff

Replace the incremental JSON-event parser with three small guards: stop
buffering rg stdout past 10MB, drop the trailing partial event before
parsing, and slice fallback context lines to MAX_LINE_CHARS. Drops the
fallback file-size skip and skip-count reporting.

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

---------

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

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag (#13522)

* chore(vscode): remove per-tool MCP auto-approve checkboxes from webview

MCP auto-approval is now governed solely by the global 'Use MCP servers'
toggle; the SDK approval path (shared with the CLI and desktop app) has no
per-tool granularity, so the per-tool and 'Auto-approve all tools'
checkboxes were no-ops that implied control that no longer exists. Remove
them from the MCP settings view and chat tool rows. The autoApprove arrays
in cline_mcp_settings.json and the toggleToolAutoApprove RPC are left
intact for the legacy extension.

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

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag

Keep the checkbox components, handlers, and RPC plumbing intact but gate
rendering behind SHOW_MCP_PER_TOOL_AUTO_APPROVE=false: the SDK approval
path (shared with the CLI and desktop app) is all-or-nothing via the
global 'Use MCP servers' toggle, so the per-tool checkboxes were no-ops.
Flip the flag back on if the SDK gains per-tool approval granularity.

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

---------

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

* fix(tools): create new files with the platform-native line ending (#13521)

* fix(tools): use platform-native EOL for new files and preserve CRLF in apply_patch updates

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

* simplify to the minimal new-file EOL fix

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

* extract shared normalizeNewFileLineEndings helper

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

---------

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

* Add suggested schedule templates to the desktop Schedules page (#13529)

* Add suggested schedule templates to desktop Schedules page

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

* Fix unreadable selected text in inputs caused by selection utility conflict

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

* Restyle Suggested section label as small gray uppercase

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

* Hide suggested schedule cards that match an existing schedule name

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

---------

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

* Disable the agent todo tool and hide the Agenda UI in the desktop app (#13530)

* remove todo tool and Agenda UI, keep schedule-only tasks tool

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

* chore: biome formatting fixes

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

* restore agenda backend; disable todo kind behind a flag instead of deleting

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

* keep agenda automation pump idle while the todo tool is disabled

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

* remove todo tool and Agenda UI altogether (revert the disable-flag hybrid)

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

* restore all agenda code to main state

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

* disable agent todo tool and hide Agenda UI behind flags

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

---------

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

* Add Desktop App and Cloud Platform to bug report issue template (#13532)

* Add Desktop App and Cloud Platform to bug report surfaces

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

* Rename Surface Diagnostics field to Diagnostics

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

---------

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

* desktop: sidebar navigation cleanup with New/Schedule/Customize rows and dialog-based search (#13533)

* desktop: clean up sidebar navigation chrome

- Give New Task its own full-width labeled row below the logo row
  instead of an ambiguous icon next to the agenda toggle
- Wire the New Task row to the home action so starting a new task
  clearly takes you home (the logo still works as a fallback)
- Swap back/forward chevrons for browser-style arrow icons and
  bump their size

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

* desktop: sidebar New/Schedule/Customize rows and always-visible search

- Stack New (plus icon), Schedule, and Customize as full-width labeled
  rows below the logo; whole row highlights on hover via sidebarItem
- New starts a fresh task (home), Schedule opens Settings > Schedules,
  Customize opens the Customizations sections (Plugins first)
- Show the session search bar permanently above the sessions list
  instead of hiding it behind a search icon toggle

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

* desktop: move session search into a dialog behind a logo-row icon

- Replace the inline sidebar search bar with a search icon in the
  logo row that opens a cmdk command dialog listing sessions
- Selecting a result opens that session and closes the dialog
- Remove the agenda/tasks toggle the icon replaces, along with the
  now-unreachable sidebar Agenda panel (the welcome screen still
  surfaces agenda tasks)

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

* desktop: load full session history when the search dialog opens

Addresses Greptile review on #13533: the dialog only searched the
currently loaded history batch, so older unloaded sessions could not
be found. Opening search now kicks off loadAllSessions() (the hook's
purpose-built global-search loader), and the empty state reads
'Searching older sessions...' while more history is streaming in.

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

---------

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

* Hide Channels and Agents sections from desktop app sidebar (#13527)

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

* Desktop app: organize sidebar sessions into Pinned, Scheduled, and Tasks sections (#13528)

* Add Pinned/Scheduled/Tasks categories to desktop app sidebar

Replace the Schedules and Favorites filter-menu options with visible
collapsible category sections in the session sidebar, and rename the
Favorite action to Pin across the sidebar and sessions view.

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

* Grow full history window when Tasks show-more outpaces loaded tasks

loadMoreSessions treats its argument as a limit on all sessions, but the
Tasks show-more count only tracks Task rows, so once pinned/scheduled
rows pushed the loaded total past the requested count the call no-oped
and clicks went dead. Grow the whole history window via
loadOlderSessions instead, and only when the loaded tasks cannot fill
the next page. Addresses Greptile review on #13528.

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

* Auto-fill the Tasks page instead of fetching once per show-more click

A single 50-session window growth can consist entirely of pinned or
scheduled sessions, leaving a show-more click with no visible Tasks
progress. Replace the one-shot fetch with a page-fill effect that keeps
growing the history window until the requested Tasks page fills or
history runs out. Addresses the follow-up Greptile review on #13528.

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

* Halt page-fill retries after a failed history fetch

A failed fetch leaves the task count and has-more state unchanged,
which are exactly the conditions the page-fill effect fires on, so one
failing request would retry and re-toast forever. Halt the effect after
a failure and let the next explicit show-more click retry. Addresses
the third Greptile review on #13528.

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

---------

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

* Redesign desktop Model Providers page and split voice input into its own settings page (#13531)

* Redesign desktop Model Providers page and split voice input into its own settings page

- Group providers into Connected / Popular / All with auth-kind hints and
  connection status instead of per-row enable toggles
- Show browser sign-in (not an API key field) for OAuth providers, with a
  collapsed manual-key escape hatch where supported, plus explicit
  Connect / Disconnect / Sign out actions
- Move voice input to a dedicated Settings > Voice page that only offers
  connected transcription-capable providers, preselects a default model
  (streaming preferred), and stays disabled in the sidebar until a
  provider is connected

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

* Show native tooltip on the disabled Voice settings nav item

Disabled buttons drop pointer events, so the 'connect a model provider'
hint moves to a wrapping span for the browser tooltip to render.

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

* Drop letter avatars and gray provider ids from provider rows and voice chips

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

* Drop model counts from provider list rows

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

* Rename provider Connected status to Configured and drop the green styling

A settings entry is configuration, not a live connection; neutral gray
text avoids implying an active link, since the user still picks which
configured provider to use per chat.

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

* Resync provider catalog from disk when a settings save fails

Connect/disconnect/credential edits update the list optimistically; a
failed save now reloads the catalog instead of leaving the optimistic
state (and the view's module cache) claiming a configuration that was
never persisted.

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

* Rename oauthProvider test fixture to dodge CodeQL name heuristic

CodeQL's clear-text-storage query flags any identifier matching 'oauth'
as a credential source and traced the fixture's provider id into the
favorite-models localStorage write, which stores only provider/model id
strings. Renaming the fixture removes the false-positive source.

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

* Guard catalog reloads against races and resync detail drafts on failed saves

Optimistic provider mutations now bump a generation that discards any
in-flight catalog response, so a failed-save recovery reload can't
overwrite a newer edit with an older disk snapshot. The recovery also
remounts the provider detail panel via a reset token so its local field
drafts reflect the reloaded on-disk state instead of unpersisted edits
or an optimistically cleared disconnect.

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

* Fix failed-save recovery ordering and retry superseded reloads

Remount the provider detail only after the authoritative catalog reload
lands, so its drafts re-seed from disk state rather than the optimistic
values that failed to persist. When a concurrent edit supersedes the
recovery's in-flight response, retry the reload (bounded) instead of
dropping it, since that edit performs no reload of its own.

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

---------

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

* feat(desktop): Customize hub, sidebar overhaul, and settings polish (#13538)

* feat(desktop): merge customization pages into a Customize hub with inline marketplace

Replaces the Plugins page and the dedicated Marketplace page with a single
Customize hub. Tabs: Skills, MCP, Plugins, Rules, Hooks, Tools, each with
live counts. Tabs backed by a marketplace catalog render the installed
items followed by an inline browsable Browse section (CLI-hub style), so
installing from the catalog immediately reflects in Installed above.

- Installed cards restyled to mirror the browse-card anatomy: bg-card p-4
  containers, absolute top-right xs Uninstall matching Install, truncating
  semibold titles, primary-tinted icons, real Badge components instead of
  ad-hoc bordered spans, un-indented line-clamped descriptions
- Rules/Hooks/Tools rows brought into the same card language; redundant
  intro paragraphs (duplicating the page description) removed; Tools group
  headers match the Installed header style with counts
- Marketplace section header renamed to Browse; duplicate 'N results' row
  removed (the header count is the single source)
- MCP embedded view now shows the full marketplace instead of
  installed-only

* feat(desktop): overhaul sidebar sessions and navigation

Sessions list:
- Sort toggle removed; sessions are always grouped by project, with pinned
  sessions leading each group (both subsets ordered by recency). The
  Pinned/Scheduled/Tasks category sections and their time-mode paging
  machinery (page-fill effect included) are deleted
- Scheduled sessions get an inline clock icon next to the pin position;
  pin + clock render together when both apply, and the running/unread
  status dot now coexists with them
- One font size (text-sm) across the list: titles, timestamps, project
  headers, show-more buttons, empty states. sidebarText needed !text-sm
  because the default button size's text-base wins the twMerge conflict
- Gradient fade under the Sessions header once the list scrolls, so rows
  fade out instead of hard-clipping
- The session-detail hover card is controlled from the sidebar and closes
  on scroll (Radix receives no pointer events while scrolling, so it used
  to float over moving content)
- Sidebar min resize width raised 224->260px; the per-project show-more
  label truncates so its nowrap text can't force rows to overflow and clip
  timestamps at narrow widths

Navigation:
- Customize replaces the Plugins/Marketplace/Hooks/Rules/Tools sidebar
  entries; Schedules and Customize are hidden from the expanded settings
  nav (their top rows cover them) but stay reachable when collapsed
- The settings gear always opens General instead of resuming the last
  section; the Account no-op hover special case is gone
- The New row highlights (aria-current) while the fresh not-yet-started
  task page is showing and hands off to the session row once the task
  starts; hitting New also focuses the prompt input via a window-event
  signal (lib/prompt-input-focus.ts) since the sidebar and composer sit in
  distant subtrees
- Fixed the xs button size collapsing any icon-bearing button to 12x12
  (leftover has-[>svg]:size-3 from when xs was a micro button) — this was
  why Uninstall buttons rendered broken next to Install

* feat(desktop): polish settings pages and chat composer

Models page:
- The provider detail panel is always open: no X button, no empty
  no-selection state. It defaults to the first connected provider (falling
  back to the first in the catalog), which also removes the layout shift
  that happened when the page swapped between full-width and panel
  variants on selection
- Fixed the list pane becoming unscrollable while the panel was open:
  grid items default to min-size auto, so the pane grew past its track
  inside the overflow-hidden grid and its ScrollArea had nothing to
  scroll; wrapped it in a min-h-0 min-w-0 cell
- Add Provider opens a Dialog instead of swapping the page
  (AddProviderContent gained a dialog variant that renders only the form)
- Embedded inputs (provider search, model search, detail fields) share one
  EMBEDDED_INPUT_CLASS stripping the Input component's own border/dark bg
  tint/shadow/ring, which rendered as a mismatched inner box; the model
  search box uses the same h-9/px-3 frame as the provider search
- Model list flows with the page instead of a max-h capped inner scroller

Other pages:
- Account uses the shared PageFrame/PageHeader: left-aligned, text-3xl
  title, Sign Out in the header actions slot
- Desktop notifications is one General section: header row plus the
  Event/Notify/Sound matrix nested in a card, so its rows no longer read
  as top-level peers of Dark mode; 'Available in the desktop app' label
  removed
- Schedule page retitled from Schedules with a real description; Customize
  description rewritten

Chat composer:
- The voice dictation button only renders once a voice model is
  configured (Settings -> Voice); the unconfigured deep-link state is
  gone (prop type kept for an easy restore)

* chore(desktop): release v0.0.17

* fix(desktop): unblock sdk-test lint on the voice-input model picker (#13553)

The model picker renders a radiogroup of styled buttons with role=radio
and aria-checked; biome's useSemanticElements flags the role as an
error, which fails the sdk-test Quality Checks lint for every PR
touching sdk/ or apps/ paths. Suppress with a justification — switching
to input type=radio needs a restyle and belongs to the desktop settings
work.

* fix(vscode): include rich workspace metadata in system prompt (#13518)

* capture richer workspace information for vs code extension

* fix(shared): redact credentials from workspace remotes

* fix(shared): avoid regex backtracking in remote redaction

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>

* Hide task costs on vscode when ClinePass is selected (#13515)

* fix: stop showing cost estimates for subscription-billed providers (#13552)

* fix(vscode): stop showing cost estimates for subscription-billed providers

Providers whose usage is covered by a flat-rate subscription (ChatGPT
Plus/Pro via openai-codex, ClinePass) are marked with
metadata.usageCostDisplay = "subscription" in the SDK, and the CLI
already suppresses dollar figures for them. The VS Code host collapsed
that value into "show" before it reached the webview, so the task
header and model pricing rows rendered API-rate cost estimates that
users read as real charges on top of their subscription.

Pass all three usageCostDisplay values ("show" | "hide" |
"subscription") through the catalog listing and render cost only when
the value is "show", matching the CLI's shouldShowCliUsageCost
policy.

* feat(llms): mark Claude Code as a subscription-billed provider

Claude Code is typically authenticated with a Claude Pro/Max
subscription, but its models reuse Anthropic API pricing metadata, so
Cline rendered per-token prices and API-rate cost estimates for usage
that is covered by the subscription. Set usageCostDisplay =
"subscription" on the provider (picked up by the CLI and the VS Code
webview) and suppress the price rows in the Claude Code settings card.

The Claude Code CLI can also run on API-key billing, where a real cost
exists; the provider cannot distinguish the two, so we prefer showing
no number over a misleading one.

* fix(vscode): suppress cost display until provider listings load

While the ListProviders request is in flight (or after it fails), the
usage-cost hook had no listing to consult and fell back to "show",
flashing the API-rate estimate at subscription users on every chat-view
mount — the exact display the previous commit removes. Return
"unknown" whenever listings are absent; consumers already render cost
only for "show", so they suppress it during that window with no
changes. Briefly hiding a real cost is harmless, briefly showing a fake
charge is not.

* fix(desktop): reconcile voice settings after main sync

* test(llms): allow experimental ElevenLabs models

* fix(sdk): preserve canonical media model behavior

* feat(desktop): customize macOS DMG install window (#13563)

* feat(desktop): add Retina DMG background tooling

* feat(desktop): customize the macOS DMG layout

* ci(desktop): validate DMG background assets

* fix(desktop): adjust DMG Applications icon position

* ci(desktop): drop redundant DMG artwork validation from publish workflow

Tauri's beforeBuildCommand already runs dmg:background (with its own
validation) at the start of the build/sign/notarize step, and the
release/beta config overlays do not override the build section, so this
step duplicated work the publish job performs anyway. PR-time coverage
lives in desktop-test.yml.

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>

* feat(desktop): sidebar time view, Customize/Marketplace split, and schedule page UX (#13570)

* feat(desktop): split Customize into Installed and Marketplace pages

The Customize hub previously embedded a Browse section inside every tab
that had a catalog. That inlining made each tab long and buried the
catalog. Customize is now the installed inventory only (skills, MCP,
plugins, rules, hooks, tools tabs pass marketplaceVariant="installed"
to the embedded MarketplaceView; McpServersContent grew the same prop),
with an outline Marketplace button in the header.

Browsing moved to a dedicated Marketplace settings section that renders
the previously dead "directory" variant of MarketplaceView: one list
across all catalog types with type-filter chips, wrapping tag chips,
and light rules separating the filter tiers from each other and from
the results. The Clear control now renders inline at the end of the tag
row only while a tag is active, the Updated date is gone, and the
header hosts an Installed button mirroring the one on the Customize
page. Directory subheader copy: "A curated set of plugins, MCP
servers, and skills from the Cline community."

Tag and type chips wrap to new lines instead of scrolling
horizontally.

* feat(desktop): sidebar time view with sections, sort toggle, and scheduled detection

Restores the time-sorted session list as the default sidebar view, with
collapsible Pinned / Scheduled / Tasks sections (headers appear only
once something is pinned or scheduled) and the page-fill effect that
grows the fetched history window until a Show-more click makes visible
progress. Project grouping stays as the alternate mode behind a
one-click sort toggle whose icon reflects the active mode — the old
dropdown cost an extra click for a two-option choice.

Scheduled sessions are detected two ways: the hub-schedule origin
trigger in session metadata, plus a fallback that asks the hub which
session ids belong to schedule executions (list_routine_schedules,
fetched on mount and every two minutes, merged into a rolling set).
The fallback matters because locally executed scheduled runs do not
reliably stamp the trigger into session metadata — a real scheduled
session created today carried only {mode:"user"} provenance. The
scheduled clock icon now leads the row, left of the title; pin and
timestamp stay on the right.

The initial visible page grows from 10 to 30 rows so a tall sidebar
fills instead of stranding a stub of rows over empty space (history
fetches already start at 50).

The expanded sidebar's Customize row now hosts indented Installed and
Marketplace sub-tabs while a customize section is open; the active
sub-tab carries the full selected background while the parent keeps a
subtler one so the two simultaneous highlights read differently.

Also fixes the hover-card flash on click (logo card and session-row
cards): Radix HoverCardContent sits on a DismissableLayer, so a click
on the trigger registers as a pointer-down outside the card and
dismisses it, and the trigger's focus event immediately reopens it.
onPointerDownOutside preventDefault suppresses the dismissal; cards
still close on pointer leave.

* feat(desktop): schedule page row, dialog, and details UX polish

Schedule cards are now click targets: clicking anywhere on a card
outside its controls opens the details dialog (guarded via
closest("button,...") since every inline control, including the Radix
switch, renders a button element), with Enter/Space keyboard support.
The redundant eye button is gone. The remaining edit / run / pause /
delete buttons grow from the 12px icon-sm size to 28px targets with
16px icons, sized consistently with the adjacent enable toggle — the
icons use explicit size-4 classes so the Button base svg rule cannot
shrink them back.

The new/edit dialog gains breathing room between field labels and their
inputs (space-y-2 per field wrapper).

The details dialog no longer scrolls as a whole when the schedule JSON
is long: the dialog is a flex column capped at 85vh, the JSON pre
shrinks to the remaining space (min-h-0) and scrolls internally, and
the Runs tab list scrolls inside the tab the same way.

* feat(desktop): scheduled sessions UX — unified details dialog, run-now handoff, hidden steering, stuck-thinking fix (#13573)

* feat(desktop): merge schedule details into one view and open run-now sessions

The schedule details dialog drops its Overview/Runs tabs: one scrollable
column with the meta grid, the configuration JSON (capped at max-h-64
with internal scroll so it cannot crowd out what follows), and a Runs
section beneath it showing the three most recent runs with a ghost
"Show all N runs" expander (collapsed again whenever a different
schedule's details open). The "Full configuration for this schedule"
subtext is gone; the dialog passes aria-describedby={undefined} so
Radix does not warn about the missing description.

Run now hands you into the session it starts. The trigger command
queues the run and returns before the runner attaches a session id, so
after the toast the handler polls the schedule overview once a second
for up to 15 seconds — which doubles as keeping the page's run status
fresh (refreshSchedules now returns the fetched overview to make that
single-stream) — until the triggered execution reports its session id,
then calls onOpenSession. Guarded so it never auto-navigates after the
user left the page.

* feat(desktop): hide runtime steering messages from transcripts

Scheduled/automation runs inject user-role steering messages each
iteration ("[SYSTEM] This run is not complete until you call
submit_and_exit...", plus a team-obligations variant). The chat view
rendered them as user bubbles, as if the person had typed them — in a
scheduled session the transcript was mostly [SYSTEM] noise.

They are machinery talking to the model, not something the person said
or needs to read, so the transcript now hides them entirely:
MessageBubble renders null for any [SYSTEM]-prefixed user message.
Grouping still treats them as working-row machinery via a single
isSystemSteeringMessage predicate — they collapse into the run's work
span, are never a turn boundary, can never be mistaken for a run's
answer, and never advance the run count even when metadata is missing —
so work-block folding and checkpoint/edit run numbering stay correct.
A finished scheduled session now reads as prompt, work summary, answer.

* fix(desktop): poll history while an attached session's event stream is dead

Opening a scheduled session while (or right after) it runs left the
view stuck on the thinking shimmer until the user switched away and
back. Root cause is in core: the hub daemon executes scheduled runs on
a private LocalRuntimeHost inside createLocalHubScheduleRuntimeHandlers,
while the hub server only projects live events from its own session
host — so session.attach succeeds but no assistant/tool/status events
ever flow. And since multiple hub daemons share cron.db, a run claimed
by a different daemon is invisible to this hub regardless. The proper
core rewiring is tracked as ENG-2474.

Client-side heal that covers every case: while an attached history
session reports a busy status and no chat_event chunk has arrived for
five seconds (and no assistant bubble is mid-stream), poll every three
seconds — re-read canonical history, merged through the same dedupe
path hydration uses, and the session record's status — so the
transcript and the thinking indicator settle in place. Locally driven
turns keep chunks flowing, so the quiet-window guard keeps the fallback
inert there.

* chore(desktop): format workspace selector components

Biome formatting drift that landed on main; picked up by a formatter
pass over components/views/chat.

* fix(desktop): keep stale-stream poll inert during locally driven turns

The fallback poll could fire between a local submit and the model's
first chunk (optimistic user bubble added, stream quiet past the
window, no assistant bubble yet). It then replaced the optimistic
bubble — raw prompt text — with its canonical history twin, which is
stored wrapped in a user_input envelope. The rekey handler that runs
when the stream starts looks for a trailing user bubble matching the
raw prompt, finds only the wrapped copy, and appends a second bubble:
duplicated messages in normal interactive chat.

The poll now stays inert while a local turn is in flight
(turnEpoch !== turnSettledEpoch, or outstanding optimistic user
messages), checked both before polling and again after the snapshot
returns. Hydration marks the turn settled — the mount defaults
(epoch 0, settled -1) otherwise read as an open turn and would keep
the fallback inert forever for the scheduled-session case it exists
for. Applying a polled snapshot also rebuilds the live tool routing
keys, same as hydration, so later tool events update canonical rows
in place instead of appending.

* fix(desktop): keep the working indicator alive for narrating scheduled runs

Watching a scheduled run live: the first tool row appeared, then the
thinking indicator vanished with nothing streaming, and the rest of
the run (final answer, submit_and_exit) only showed up seconds later
in one lump.

inferHydratedChatStatus treats a "running" session record whose
transcript ends on an assistant message as a session that died without
a status flip and reports "completed". That heuristic is right for
stale records, but scheduled/automation models narrate between tool
calls, so a polled snapshot can genuinely end on assistant text
mid-run — the completed flip hid the working indicator, folded the
run early, and disarmed the stale-stream poll (status left the busy
set), dead-ending live updates until an in-flight poll happened to
deliver the finished run.

The heuristic now only applies once the transcript has actually gone
quiet (newest message older than two minutes — comfortably past model
latency plus tool runs). A recently active transcript keeps the
record's "running" verdict, so the indicator stays up and polling
stays armed until the record itself settles.

* fix(desktop): stale-stream poll mirrors the session record instead of inferring

Replaces the previous fix for the vanishing working indicator (the
time-window guard added to inferHydratedChatStatus) with a version
that adds no inference at all: the heuristic is restored to exactly
its long-standing form, and the poll now maps the session record's
status verbatim (mapSessionRecordStatus).

The record is the right authority in the poll's context: the sessions
this fallback serves have a live host maintaining their record, and it
flips to a terminal status when the run ends. Transcript-shape
inference belongs only where it has always lived — hydrating sessions
whose records may be orphaned — and would misread a mid-run snapshot
ending on assistant narration as a finished session, hiding the
working indicator and disarming the poll.

* fix(desktop): address review findings on steering detection and run-now matching

Steering detection additionally requires the injected-message marker
(meta.userRunSpan === 0) beside the [SYSTEM] prefix, so a person's
genuine prompt that happens to start with "[SYSTEM]" stays visible
and turn-counted. The failure direction is deliberate: an unstamped
injected reminder would merely show as a user bubble, while the
content-only check could hide a real prompt.

Run-now only follows the execution id the trigger reply itself named;
the newest-execution-for-this-schedule fallback could open a previous
run's session when the trigger failed to enqueue one.

* fix(desktop): report a failed run-now instead of confirming a start

A trigger reply without an execution means no run was enqueued (the
schedule may have been disabled or deleted since the page loaded). The
handler previously toasted "Run started" regardless and then silently
skipped the session-open polling. It now shows a destructive
"Run not started" toast, refreshes the schedule list so the row
reflects reality, and skips the polling entirely.

* fix(desktop): don't block the main thread on quit while stopping the sidecar (#13566)

Quitting the mac app beach-balled for ~5-7s. The shutdown POST was
built from the ws transport URL (appending /shutdown lands inside the
query string), so the sidecar was never told to exit, and stop() then
polled the child for up to 7s on the main thread - on macOS inside
applicationWillTerminate - before SIGKILLing it.

stop() now sends SIGTERM and returns immediately. The sidecar handles
SIGTERM with the same bounded (5s) graceful shutdown as the /shutdown
endpoint and exits itself, finishing session persistence as an orphan.

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

* feat(desktop): hover trash button on sidebar session rows (#13582)

Each session row shows a trash icon on the right while hovered (or
when the button itself is focused), opening the same delete
confirmation dialog the row's context menu uses. The row is a button
and buttons cannot nest, so the trash is an absolutely positioned
sibling inside a group/row wrapper, overlaid where the timestamp sits:
row hover hides the timestamp, shows the trash, and moves the row's
hover background to the wrapper group so it holds while the pointer is
on the trash itself.

* fix(desktop): install marketplace plugins and MCP servers in-process instead of spawning a cline binary (#13585)

* fix: install marketplace plugins and MCP servers in-process instead of spawning a cline binary

The desktop app sidecar and cline-hub shelled out to 'cline plugin install'
and 'cline mcp install' for marketplace installs. Packaged GUI apps inherit
launchd's minimal PATH on macOS and most desktop users have no cline CLI
installed at all, so installs failed with a red
'Executable not found in $PATH: "cline"' error.

Install via @cline/core's installPlugin/installMcpServer in-process instead,
matching what the VS Code extension already does. Also fix
parseMcpInstallArgs in @cline/core to treat the marketplace catalog's '--'
separator as end-of-options; previously the separator itself became the
stdio command.

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

* refactor: drop test-injection plumbing from marketplace installers

Call @cline/core's installPlugin directly instead of threading an
installer option through the marketplace entry points; tests stub the
core module instead.

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

* revert: keep cline-hub marketplace installs CLI-backed

The hub dashboard is launched via 'cline dashboard', so a CLI is always
present and CLINE_WRAPPER_PATH resolves it; the PATH bug only affects
the desktop app, which does not ship a CLI.

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

---------

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

* chore(desktop): release v0.0.18

* chore(vscode): release v4.1.16

* chore(sdk): release v0.0.80

* chore(cli): release v3.0.59

* fix(hub): stop shipping full transcripts inside broadcast hub events (#13587)

* fix(hub): stop shipping full transcripts inside broadcast hub events

Every session.updated (and session.created/detached/run.started) event
embedded the session's ENTIRE message transcript via readCoreSessionSnapshot,
even though no consumer reads snapshot.messages off an event — clients fetch
messages with the session.messages command. For a multi-megabyte transcript
this turns every status flip into megabytes per subscriber, floods the
durable event log, and (until the send-queue backpressure fix lands) lets a
slow subscriber balloon the hub process by one full transcript copy per
event — reported as a 25GB cline process on a 16GB Mac.

Strip snapshot.messages centrally in HubServerTransport.publish() so every
current and future event publisher is covered, the event log stores slim
envelopes, and cursor replay stays byte-identical with live fan-out. All
other snapshot fields (status, usage, model, workspace, checkpoint) are kept,
and command replies are untouched.

* fix(hub): never capture the transcript into event/reply snapshots

Replaces the publish-boundary strip with the real fix: don't build
message-bearing snapshots in the first place. emitSessionSnapshot no longer
re-reads the entire transcript from disk on every status flip, and
readCoreSessionSnapshot no longer reads it for any event or reply — a
snapshot is a state notification (status, usage, model, workspace,
checkpoint); the transcript is fetched via the session.messages command.
Checkpoint-restore snapshots (session-versioning-service) are untouched:
restore replies carry messages in their own dedicated field.

* chore(desktop): release v0.0.19

* chore(sdk): release v0.0.81

* chore(cli): release v3.0.60

* fix(vscode): avoid render crash on malformed api_req payloads in combineApiRequests (#13560)

* fix(vscode): stop pinning DeepSeek model count in catalog smoke test (#13600)

* feat(ui): share agent welcome hero (#13567)

* feat(ui): share agent welcome hero

* test(ui): cover welcome hero pointer states

* refactor(ui): keep welcome hero API minimal

* test(ui): verify welcome hero package assets

* fix(ui): inline welcome hero masks

* fix(tools): preserve a file's own CRLF line endings across apply_patch updates (#13512)

* fix(desktop): keep the window title bar draggable across views (#13572)

* fix(desktop): keep window title bar persistent

* fix(desktop): reserve persistent title bar space

* fix(desktop): polish persistent title bar layout

* Sign Windows CLI binaries with Azure Trusted Signing; surface app-control launch errors (#13021)

* feat(cli): sign Windows binaries with Azure Trusted Signing and surface app-control launch errors

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

* fix(cli): use _CLI-suffixed signing profile secret, normalize endpoint, fail loud on partial config

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

---------

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

* Tunnel ProtoBus over the existing Host Bridge (#13218)

* feat(core): tunnel ProtoBus over Host Bridge

* fix(core): harden Host Bridge stream lifecycle

* fix(core): serialize concurrent chunked responses per request

Streaming handlers deliver updates fire-and-forget, so two logical
responses for one request_id can be in flight at once. Chunked payloads
made forwarding non-atomic: each chunk write is an await, so concurrent
forwards could interleave their chunk sequences and the receiver --
which reassembles purely by arrival order -- would splice two payloads
into one. Route all forwards for a request through one promise chain; a
failed write rejects every later forward so a torn payload is never
followed by more chunks.

Rename the lock manager's instanceAddress to instanceOwner: it holds an
opaque per-spawn instance ID on the token path and a listener address
only on the CLI-harness path. Delete the caller-less getInstanceByPort
query that interpreted the owner as an address.

Also: document message_json as a legal wire encoding for small
payloads, close the gRPC client when startup fails, note the
intentional discard of the cancellation confirmation, and add the
proto's trailing newline.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* Build and Authenticode-sign a Windows x64 desktop installer in desktop releases (#13607)

* feat(desktop): build and Authenticode-sign a Windows x64 NSIS installer in desktop releases

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

* fix(desktop): pin OIDC-adjacent actions to commit SHAs in the Windows signing job

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

* fix(desktop): pin checkout and upload-artifact to commit SHAs in the Windows signing job

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

---------

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

* fix(desktop): show agent-created schedules on the Schedules page (#13613)

* fix(desktop): show agent-created schedules on the Schedules page

Schedule hub commands are scoped to the workspace registered by the
connection, but the desktop app's hub client registers the app launch
directory while agent-created schedules live under each chat's own
workspace folder - so they never appeared on the Schedules page.

Grant token-authenticated hub connections (which can already bind any
workspace at registration) explicit cross-workspace schedule access via
an allWorkspaces payload flag, and have the desktop sidecar request it
for routine schedule commands. Workspace-bound clients (local browser
origins) and default CLI behavior stay scoped.

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

* refactor(core): strip allWorkspaces flag from schedule inputs and pin it in the sidecar payload

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

---------

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

* Make suggested routine template prompts prescriptive about their final output (#13611)

* Make bug hunter routine template prescriptive about its final report

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

* Make remaining routine templates prescriptive about their final output

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

---------

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

* Desktop: surface scheduled-task final output — auto-expand submit_and_exit and render its summary as markdown (#13612)

* desktop: auto-expand submit_and_exit and render its summary as markdown

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

* desktop: render submit summary in full foreground color

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

* desktop: label the submit row 'Scheduled task completed'

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

* desktop: label errored submit_and_exit rows as failed

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

---------

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

* Add tooltips explaining Live and After recording badges on voice input models (#13610)

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

* Remove box shadow from chat message actions row (#13630)

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

* fix(desktop): make the Tauri shell work on Windows (#13632)

- Defer updater installation to the user-initiated restart on Windows:
  install() launches the NSIS installer and exits the process immediately,
  so the background cycle now downloads only and stages the bytes, and
  restart_to_apply_update installs them after stopping the sidecar.
- Spawn child processes (sidecar, git, cmd /C start) with CREATE_NO_WINDOW
  so the GUI-subsystem app doesn't pop visible console windows.
- Fall back to USERPROFILE when HOME is unset resolving the MCP settings
  path, matching the sidecar's homedir().
- Reap the sidecar after the Windows hard-kill so its exe file lock is
  released before the NSIS installer replaces it.

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

* fix(core): stop watching agenda spec dirs while the todo tool is disabled (#13629)

* fix(core): stop watching agenda spec dirs while the todo tool is disabled

Since #13530 disabled the agent todo tool, the Agenda UI, and the
automation pump, the hub still created fs.watch watchers on the global
agenda specs dir and on every workspace root recorded in the task store
(at startup and on scope access). Nothing consumes the watcher-driven
task events while the feature is off, and the task.* hub commands
already reconcile spec files on demand, so the watchers are pure
overhead - one OS watch handle per known workspace.

Wire watchFiles to AGENDA_TODO_TOOL_ENABLED the same way
automationEnabled is, preserving a host's explicit watchFiles opt-out
for when the flag is turned back on. Schedules are unaffected: the
schedule list has no file watcher and updates through hub commands and
published schedule events.

* fix(core): reconcile external spec edits inside updateTask

With the spec watchers off there is no background reconciliation, so a
task spec edited directly on disk made every same-store task.update fail
the signature check with "task spec changed outside the manager" until
an unrelated task.get or task.list happened to reconcile the scope.

Reconcile the task's scope at the start of updateTask (mirroring what
refreshAndVerifyTaskIntent already does for approve/run), skipping it
when the file reconciler itself is the caller to avoid recursing from
reconcileFileStore. An external edit now surfaces as the store's normal
stale-revision conflict, and a re-read-and-retry succeeds. This also
closes the pre-existing watcher debounce race for updates.

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently (#13565)

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently

Port the cline-provider refresh semantics to openai-codex and oca:
a transient refresh failure (network error, timeout, server 5xx) with an
already-expired access token now rethrows instead of returning null.
A null return means the refresh token was REJECTED and re-auth is
required; treating an outage blip as a rejection is what turned it into
a forced 'openai-codex requires re-authentication.' task stop while the
settings UI still showed the user as signed in.

Both providers also emit user.auth_refresh_soft_failure telemetry on
transient failures (the 'prevented logout' counter the cline provider
already has) and attach status/errorCode details to the genuine
invalid_grant logout event.

* refactor: collapse duplicate soft-failure telemetry branches and test

Review feedback: compute tokenExpired once and emit the soft-failure
event once in both providers, then return current credentials or
rethrow. Fold the codex soft-failure telemetry assertions into the
existing still-usable-token test instead of a near-duplicate case.

* fix: make OpenAI Codex (ChatGPT subscription) sign-in fail loudly instead of silently dead-ending (#13537)

* fix: make OpenAI Codex sign-in fail loudly instead of silently dead-ending

When callback port 1455 is already in use (e.g. by the Codex CLI or a
previous pending sign-in), startLocalOAuthServer returns a no-op server
and loginOpenAICodex would open the browser anyway, then dead-end:
the callback could never be received, and in the VS Code extension the
user just saw nothing happen after clicking 'Sign in to OpenAI Codex'.

- loginOpenAICodex now fails fast with an actionable 'port in use'
  error before opening the browser, unless the host provides manual
  code entry (the CLI's paste fallback keeps working)
- surface OAuth redirect errors (e.g. access_denied) instead of
  collapsing them into 'Missing authorization code'
- the extension dedupes concurrent sign-in clicks: a re-click re-opens
  the auth page of the pending flow instead of spawning a second flow
  that would collide with our own callback server
- browser-open failures now show an error message with the URL to
  open manually instead of only logging
- abandoned-flow timeouts no longer surface a confusing 'Missing
  authorization code' toast

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

* refactor: drop host-side codex login dedupe, keep flow identical to CLI

The SDK owns the failure handling now (fail-fast on an unbindable
callback port), so the extension keeps the exact same simple
loginOpenAICodex call the CLI uses. A second click while a flow is
pending gets the SDK's clear port-in-use error, same as running
'cline auth openai-codex' twice would. Keep only the CLI-parallel
onOpenUrlError surfacing (the CLI prints 'open the URL above
manually'; the extension's equivalent is an error toast with the
URL).

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

* test(e2e): cover Codex sign-in callback-port failure and redirect errors

Two driven-VS Code tests for the OpenAI Codex (ChatGPT subscription)
sign-in flow:

- with port 1455 occupied on both loopback families, clicking the
  sign-in button surfaces the fail-fast port-in-use toast
- with the port free, the callback server binds and an OAuth redirect
  error (access_denied) propagates to a visible error toast

The second test opens a real browser tab to the OpenAI auth page as a
side effect of the genuine sign-in click.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* feat(core): anchor agent-created schedules in the user's .cline schedules home (#13634)

* feat(core): anchor agent-created schedules in the user's .cline schedules home

Agent-created schedules inherited whichever workspace folder the chat
session happened to run in, scattering user-level routines across chat
and project folders. They were invisible to workspace-scoped listings
elsewhere, tied to folders that may be cleaned up, and each chat's
tasks tool saw a different set when checking for duplicates.

Anchor them in ~/.cline/schedules instead: the hub's scheduled-task
session defaults now resolve to that home (created on demand), so
agent-created schedules live and run in one stable user-level scope.
The tasks tool guidance now tells agents that scheduled sessions run in
the schedules home, so prompts must carry absolute paths to any project
they operate on.

Schedules created explicitly with a workspace (CLI --workspace, desktop
routine wizard) are unchanged, and existing rows keep their current
workspaceRoot - they stay visible through the all-workspaces listing
paths (#13613, #13633).

* test(core): restore any pre-existing CLINE_DIR after the agenda hub test

The test's cleanup deleted CLINE_DIR outright, so an environment that
had it configured would leave later tests in the same worker on the
default storage directory. Save the previous value and restore it.

* test(core): restore CLINE_DIR even when hub test setup throws early

Restoring the override in the try/finally missed failures thrown during
transport construction or start(), before the try was entered. Register
the restore with onTestFinished instead, which runs regardless of where
the test fails.

* fix(desktop): don't show providers as configured without real credentials (#13608)

* fix(desktop): don't show providers as configured without real credentials

The desktop settings marked any provider with a persisted settings entry
as Configured, but legacy VS Code migration and empty saves can seed
entries (e.g. qwen-code, sapaicore) holding only a default model and no
credentials. Move the CLI's isProviderSettingsUsable readiness check into
@cline/core, expose it as a computed 'configured' flag on the provider
catalog, and use it in the desktop's isProviderConnected.

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

* fix(desktop): resync catalog after saves so Configured badge updates live

Optimistic provider mutations can't know the sidecar-computed 'configured'
flag, so after connecting a keyless provider or saving cloud credentials
(e.g. a Vertex project id) the row stayed 'Not configured' until remount.
Silently refetch the catalog after each successful save, guarded by the
existing generation counter so newer edits discard stale responses.

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

* fix(desktop): claim a generation in post-save resync so overlapping refreshes can't apply stale snapshots

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

* fix(desktop): bump catalog generation on OAuth login success

Every other optimistic provider mutation claims a new generation; the
OAuth success path didn't, so a catalog load or resync still in flight
could arrive late and overwrite the just-connected state.

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

* fix(desktop): resync catalog after OAuth login instead of bare generation bump

The resync claims a new generation (discarding any stale in-flight
response) and its own fetch covers both the new OAuth connection and any
provider saved moments earlier, matching the post-save path.

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

---------

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

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint (#13626)

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint

Restoring a checkpoint runs git reset --hard, which moves the current
branch pointer. If commits were made after the checkpoint (by the user
or by the agent), the reset silently knocked them off the branch,
leaving them reachable only through the reflog.

Guard the reset: if HEAD no longer matches the commit the checkpoint
was created on, throw a descriptive error (including how many commits
would be dropped) instead of destroying history. Chat-only restore is
unaffected, and users who really want to discard the commits can reset
the branch manually first.

Fixes #13550

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

* fix(core): close the guard-to-reset race with an atomic ref update

The moved-HEAD guard read HEAD, ran further git commands, then reset
unconditionally, so a commit landing in that window could still be
knocked off the branch. Replace the reset's branch move with git's
native compare-and-swap (git update-ref HEAD <new> <old>), which fails
if HEAD no longer points at the verified commit, and follow with a bare
reset --hard to sync the index and worktree to the already-moved HEAD.

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

---------

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

* fix: hide history cost estimates for subscription-billed tasks (#13562)

* fix(vscode): hide history cost estimates for subscription-billed tasks

The task-header fix for subscription providers cannot reach history:
history rows render the stored totalCost (an API-rate estimate) and do
not know which provider ran the task, so the history page printed
$X.XXXX on every row and the recent-task chips in an empty chat view
rendered a $ chip even for subscription-billed tasks.

The SDK session records already persist the provider — the CLI's
history view uses it for exactly this — but the VS Code mappers dropped
it. Map it through both transports (HistoryItem.apiProvider for the
state-pushed taskHistory, TaskItem.api_provider for getTaskHistory) and
suppress the dollar figure per row when that provider's
usageCostDisplay is not "show", via a new useUsageCostVisibility
predicate shared by both surfaces.

Rows without a recorded provider (tasks predating the field, legacy
imports) keep showing the stored value — there is nothing to key
suppression on.

* test(vscode): e2e-verify history cost suppression in real VS Code

Seeds SDK session records (one openai-codex subscription task, one
anthropic usage-billed task) into the isolated CLINE_DIR before the
webview loads, then asserts in a real VS Code instance that both the
recent-task chips and the full history page render the dollar figure
only for the usage-billed task. Covers the two boundaries the unit
tests stub: on-disk records reaching getTaskHistory with provider
populated, and the provider listings delivering the subscription mark
to the webview.

* Fix scheduled tasks disappearing after desktop app updates (#13627)

* Fix hub-managed schedules being wiped by cron reconciliation on hub restart

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

* Require the virtual hub/schedules path when exempting specs from removal reconciliation

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

* Treat recorded source mtime as proof a spec is file-backed, closing the hub/schedules spoof gap

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

---------

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

* fix(shared): discover global rules at ~/Cline/Rules (#13614)

The VS Code Rules tab resolves the Documents folder via
'xdg-user-dir DOCUMENTS', which prints bare $HOME when no user-dirs
config exists (WSL/headless installs), so it reads and writes global
rules at ~/Cline/Rules. The SDK's rule search paths only covered
~/Documents/Cline/Rules, so those rules never reached the system prompt.
Add the missing path to the search list.

Fixes #13542

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

* feat: add searchable session history (#13420)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

---------

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

* Fix CLI crash when a remote MCP server is offline but enabled (#13639)

Remote (SSE/streamable HTTP) MCP connects run on the session.create
critical path, which the hub caps at 30s. Without a connect budget an
unreachable server spent the full 60s default request timeout (with the
SSE transport stuck in a reconnect loop), stalling session.create past
the hub deadline and tearing the whole session down - the interactive
TUI exited and one-shot runs failed. Stdio servers already have a
bounded initialize budget for exactly this reason; give URL clients the
same treatment with a 10s default connect budget that an explicit
timeout overrides in either direction.

Fixes #13597

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

* test(vscode): prevent E2E worker teardown hangs (#13644)

* test(vscode): capture external URLs in E2E runs

* docs(test): clarify browser capture rationale

* Add a GitHub integration step to the onboarding (#13225)

* Add feature flags to the app

* React to account updates

* Address comments

* Add a GitHub integration step to the onboarding

* validate domain and fix errors on auth

* Hide the step behind a feature flag

* update version

---------

Co-authored-by: John Choi <john.choi@cline.bot>

* fix(ci): stop e2e worker teardown timeouts and deflake hub daemon e2e on Windows (#13646)

* fix(e2e): stop VS Code e2e worker teardown from timing out

The ext-vscode-test-e2e job has been failing on main with 'Worker teardown
timeout of 60000ms exceeded' even though every test passes. Playwright only
reports an Electron app as closed once the process exits AND every holder of
its stdio pipes is gone (ChildProcess 'close' waits on the extra fd3/fd4
pipes Playwright creates for Electron). Any VS Code descendant that outlives
the main process (chrome_crashpad_handler, GLib's 'dconf watch' helper,
xdg-open browser handlers, VS Code 1.135's agent host CLI subprocess that
logs 'unable to kill the process') keeps those pipes open, so app.close()
never resolves and the worker teardown hangs on it until its 60s timeout
fails the job.

Harness fixes, each removing one source of that wedge:

- closeAppForTeardown now SIGKILLs the whole process group (taskkill /T on
  Windows) when app.close() times out, instead of only the main pid — and
  does so even when the main process already exited, which is exactly the
  wedged state. Playwright launches Electron detached, so pid == pgid.
- Launch VS Code with --disable-crash-reporter so no crashpad handler
  outlives the app holding the harness pipes.
- Seed the fresh user-data-dir with chat.disableAIFeatures: true so VS
  Code's own AI features (rolled out via server-side experiments, so CI
  breaks without any repo change) never start their agent host process.
- Drop the page.close() teardown: closing VS Code's last window quits the
  whole app, and ElectronApplication.close() on an already-exited app
  deadlocks; the app fixture's app.close() closes windows itself while the
  app is alive.
- Codex sign-in no longer opens a real external browser under E2E_TEST; the
  codex-oauth test drives the OAuth callback itself, and the browser was an
  orphaned process holding the harness pipes on the runner.

* fix(core): deflake hub daemon e2e tests on Windows runners

sdk-test on windows-latest fails intermittently in the hub daemon e2e
files:

- shutdown.e2e.test.ts dies with a bare 'Error: socket hang up'. That
  message is the ws handshake (http.ClientRequest) failing, not the
  /shutdown fetch (an undici failure prints 'TypeError: fetch failed'):
  a freshly spawned bun daemon on a loaded 2-core Windows runner
  occasionally drops its first accepted connection before writing the
  upgrade response. Real hub clients reconnect with backoff, and the test
  asserts shutdown behavior rather than first-connection reliability, so
  openAuthenticatedSocket now retries transient handshake failures within
  a 15s budget.
- singleton.e2e.test.ts times out waiting for daemon discovery: it still
  used the 10s hang guard that 0cfc90158 already raised to 30s in
  shutdown.e2e.test.ts for the same reason. Use the same 30s guard.
- Raise the e2e testTimeout to 60s so a test that legitimately spawns two
  daemons back to back can survive slow-runner startups instead of the
  discovery hang guard being cut off by the test timeout.

* feat(desktop): render tool output images as attachments (#13643)

* fix(desktop): render tool output images as attachments

Add support for displaying media returned by tool calls (e.g. screenshots)
as rendered images with expand-to-fullscreen capability instead of raw
base64 text. Introduces an `ImageCarousel` component for navigating
multiple images, propagates the expand handler to tool message blocks,
and extracts/validates output media in tool summaries.

* test: cover multi-image and canonical media extraction in tool output (#13645)

extractOutputMedia and the desktop tool-message rendering path were only
ever exercised with exactly one distinct valid image, and
canonicalInlineMedia (MCP-style type: "media" blocks for audio/video/file)
had zero coverage. Add tests for: multiple distinct images in one tool
result (parser + desktop carousel navigation), inline audio via the
mime_type key spelling, canonical video/file media blocks, and rejection
of an invalid canonical image block.

---------

Co-authored-by: Harrison <harrison@cline.bot>

* chore(desktop): release v0.0.20

* feat(sdk): add discovery boundary ahead of Agent Plugins support (#13017)

* ENG-2490: Propagate session aborts to teammates (#13647)

* fix(core): propagate session abort to teammates

* fix(core): persist aborted teammate tasks as cancelled

* fix(core): settle teammate work on session abort

* fix(core): isolate replacement runs from stale aborts

* refactor(core): narrow teammate task status metadata

---------

Co-authored-by: abeatrix <beatrix@cline.bot>

* fix(llms): use AI SDK 7 Langfuse telemetry (#13651)

* fix(llms): use AI SDK 7 Langfuse telemetry

* test(llms): cover Langfuse runtime context

* chore(llms): built-in model list update 1787907289186 (#13663)

* chore(llms): built-in model list update 1787907289186

Result of `bun run build:models`.
Includes updated model list and fixed formatting issues across codebase.

* test(llms): update GLM reasoning toggle expectation

* test: cover session search fallback on hub timeout and rejection (#13642)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

* test: cover sidecar search fallback on hub timeout and rejection

The existing search_sessions tests only exercised the index-hit and
empty-index-fallback paths with an immediately-resolved hub reply.
Add coverage for the two other realistic Hub-connection failure
modes the fallback is meant to tolerate: the hub call rejecting, and
the hub call hanging past the 750ms withSearchDeadline race.

---------

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

* fix(core): refresh Cline models from live catalog (#13670)

* feat(ui): share attachment drop zone (#13672)

* feat(ui): share attachment drop zone

* fix(ui): cancel disabled attachment drops

* chore(ui): simplify drop zone surface

* chore(ui): release v0.2.0-next.8

* Chore/bump undici mermaid (#13675)

* chore(deps): bump mermaid to 11.16.1 and raise undici floor to 7.29.0

* chore(deps): patch js-yaml and body-parser in the npm-managed subprojects

* fix(llms): make Langfuse tracer detection survive minified release builds (#13680)

* fix(llms): recognize direct tracer providers

* fix(llms): make Langfuse tracer detection survive minified release builds

Release binaries are compiled with minify enabled, which renames classes,
so initializeLangfuseTelemetry's constructor-name guard never matched
"ProxyTracerProvider" and silently returned readiness=false in every
production build (hub log: "creating span processor" followed by
"initialized readiness=false" with no branch message in between). Dev runs
execute unminified source, which is why the same env vars worked there.

Replace every constructor-name comparison with checks that survive
minification: detect the proxy structurally via getDelegate, distinguish a
recording provider from the no-op fallback by its lifecycle methods, and
confirm our NodeTracerProvider registration by object identity. When a
foreign provider already owns the global slot, attach the Langfuse span
processor to it when it accepts processors, and otherwise shut down the
orphaned provider and report the rejection instead of bailing silently.

Verified by bundling the module with Bun minify:true against the real
OpenTelemetry packages: the previous code reproduces readiness=false
(provider class name mangles to "H2"), the new code initializes with
readiness=true.

* fix(vscode): prevent hook spawn failures from crashing the core process (#13422)

* fix(vscode): prevent hook spawn failures from crashing the core process

A hook child-process spawn failure emitted "error" on HookProcess with no
listener registered, which Node's EventEmitter turns into an uncaught
exception - killing the entire cline-core process instead of failing the
one hook open. Guard the emit behind listenerCount so the rejection (which
StdioHookRunner handles) is the only propagation path.

The trigger was a workspace root that no longer exists on disk passed as
the spawn cwd: Node reports a nonexistent cwd as a misleading ENOENT on
the launcher binary ("spawn /bin/sh ENOENT"). Validate cwd existence in
HookProcess right before spawning - falling back to no explicit cwd with
a warning that names the missing directory - and when a spawn still fails
ENOENT because the directory vanished in between, name it in the error
message instead of blaming the shell.

* fix(vscode): fail hooks with a missing working directory instead of relocating them

Running a hook whose assigned cwd no longer exists from the host
process's own working directory would let its relative paths read and
write an unrelated location (e.g. the IDE install directory). Reject
before spawning, with an error naming the missing directory; the runner
reports the hook as failed and the task continues. Also carry pre-spawn
failure messages into HookExecutionError details so the cause is not
reduced to a bare "exited with code 1".

* fix(vscode): thread task id into hook runner creation so execution telemetry fires (#13547)

The SDK hooks adapter created every hook runner without a task id, and
StdioHookRunner gates all captureHookExecution calls on one being set —
so the next variant emitted zero hooks.execution events while discovery
telemetry fired normally. Pass the task id (and tool name for the tool
hooks) at all five factory.create call sites, and pin the threading
with a regression test.

* Desktop marketplace redesign: two-pane explorer with full catalog metadata (#13653)

* feat(desktop): add marketplace design exploration prototypes (storefront, explorer, registry)

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

* fix(desktop): render catalog icon tiles without percentage padding

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

* feat(desktop): drop placeholder icon tiles from explorer marketplace direction

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

* feat(desktop): make explorer the marketplace view, drop design exploration harness

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

* feat(desktop): add category tag filters to marketplace explorer

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

* feat(desktop): collapse marketplace category pills behind a more toggle

* feat(desktop): remove maturity badges and CLI install section from marketplace

---------

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

* fix(core): propagate parent aborts to delegated subagents (#13677)

* fix(core): propagate parent aborts to delegated subagents

* docs(core): narrow delegated abort guarantees

* fix(core): release delegated sessions after execution

* fix(core): scope abort listeners to active runs

* fix(core): inherit parent runtime pid for subagents

* fix(desktop): keep Stop available for running child agents (#13678)

* fix(desktop): keep Stop available for running child agents

* fix(desktop): reconcile aborted tool activity

* fix(desktop): guard abort and agent polling races

* fix(desktop): preserve authoritative abort status

* fix(desktop): track queue-verified completion

* test(desktop): trim duplicate abort coverage

* fix(desktop): settle delayed queue verification

* fix: sanitize stored API keys and make provider credential rejections actionable (#13549)

* fix(vscode): sanitize pasted provider API keys at the settings write boundary

Clipboards smuggle control and invisible formatting characters (newlines,
zero-width spaces, BOM) into pasted API keys. The masked key field hides
the corruption and providers reject the key with a 401 indistinguishable
from a genuinely wrong key. Strip those characters and surrounding
whitespace once in the provider config store write path, so both backing
stores (legacy state secrets and providers.json) receive the clean value.
A whitespace-only value now clears the key.

* feat(llms,vscode): classify provider 401/403 as auth errors and surface actionable guidance

Add an "auth" ProviderErrorClass, assigned when the HTTP layer reports
401/403 — status-only on purpose, since provider bodies can quote words
like "unauthorized" without the request being an auth failure. The class
rides the existing errorClass plumbing (finish -> run-failed ->
AgentErrorEvent), so every host receives it with no new wiring.

In the VS Code chat surface, rewrite classified credential rejections
from BYOK providers into actionable text pointing at the API key
configuration, keeping the provider's raw body as a diagnostic tail.
Raw bodies alone are dead ends: Mistral, for example, answers an
identical {"detail":"Invalid API Key"} for a wrong, empty, or
wrong-scope key. Cline-account providers keep the JSON path so the
webview still renders their auth failures as a sign-in card.

* Fix ask-question option text not wrapping (#13718)

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

* chore(desktop): release v0.0.21

* fix(core): stop an empty capability list from stripping image input (#13583)

`modelHasCapability` documents a missing or empty capability list as
carrying no signal, so each gate declares its own default. Two readers
bypassed it and read `capabilities` directly, where an empty list is not
nullish but `[].includes(x)` is false:

- the session runtime's `modelSupportsImages` metadata used
  `capabilities?.includes("images") ?? true`, so the intended fail-open
  never fired for an empty list and the file-read tool silently dropped
  every image from the request;
- `toProviderModel` projected an empty list onto `false`, telling pickers
  a model definitively lacks vision, attachments, and reasoning when
  nothing had been declared.

Both now route through the shared helpers, which state their unspecified
default explicitly: `modelSupportsImageInput` fails open for a capability
gate, and `declaredCapability` preserves `undefined` for `ProviderModel`'s
tri-state booleans. A populated list stays authoritative in both.

A thinking config now short-circuits `supportsReasoning` instead of being
OR-ed with the capability read, so its absence no longer collapses the
tri-state to `false`.

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* fix(llms): translate gateway capabilities in one place (#13584)

* fix(core): stop an empty capability list from stripping image input

`modelHasCapability` documents a missing or empty capability list as
carrying no signal, so each gate declares its own default. Two readers
bypassed it and read `capabilities` directly, where an empty list is not
nullish but `[].includes(x)` is false:

- the session runtime's `modelSupportsImages` metadata used
  `capabilities?.includes("images") ?? true`, so the intended fail-open
  never fired for an empty list and the file-read tool silently dropped
  every image from the request;
- `toProviderModel` projected an empty list onto `false`, telling pickers
  a model definitively lacks vision, attachments, and reasoning when
  nothing had been declared.

Both now route through the shared helpers, which state their unspecified
default explicitly: `modelSupportsImageInput` fails open for a capability
gate, and `declaredCapability` preserves `undefined` for `ProviderModel`'s
tri-state booleans. A populated list stays authoritative in both.

A thinking config now short-circuits `supportsReasoning` instead of being
OR-ed with the capability read, so its absence no longer collapses the
tri-state to `false`.

* fix(llms): translate gateway capabilities in one place

Three producers built gateway model definitions from catalog `ModelInfo`,
and each carried its own hand-written `switch` over the capability list.
Nothing tied them together, so they drifted:

- builtin providers always emitted a capability list, so a model whose
  catalog entry declares no capabilities became `["text"]` where the other
  producers emitted `undefined`. `modelSupportsToolCalling` fails open only
  for an absent or empty list, so that list read as an authoritative denial
  and stripped every tool definition from requests to the affected language
  models (dify, sapaicore, opencode, and the Codex CLI);
- the OpenAI-compatible path mapped an `audio` capability that
  `ModelCapabilitySchema` does not define, while the other two dropped it;
- the pass-through capabilities (`streaming`, `files`, `temperature`, ...)
  were enumerated explicitly in one, folded into `default:` in another,
  and ignored in the third.

One exported `toGatewayModelCapabilities` now serves every producer. It is
built on a `Record<ModelCapability, GatewayModelCapability | null>` rather
than a `switch`, so extending `ModelCapabilitySchema` without deciding the
new capability's mapping fails to compile instead of silently falling
through to a default.

The conformance tests walk the capability state space taken from
`ModelCapabilitySchema` itself and assert the real producers agree with the
translator, so a future producer that maps capabilities on its own fails
even when the translator's own unit tests still pass.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>

* fix(cli): keep markdown streaming prop stable to stop settle flash (#13719)

Flipping the <markdown> streaming prop from true to false when an
assistant text segment settles makes MarkdownRenderable call
updateBlocks(true), which skips every block-reuse path and destroys and
recreates all block renderables. Until tree-sitter re-highlights them
the whole message renders blank/unhighlighted, which users see as the
text flashing at the end of each response. Keep streaming={true} for
the transcript markdown (opencode's TUI does the same); entry.streaming
still drives the spinner glyph.

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

* Clarify model-facing message when user rejects a tool call (#12673)

* Clarify model-facing message when user rejects a tool call

* Include the rejected tool's name in denial reasons

* Move user-rejected tool reason into @cline/shared

* Route new user-rejection approval paths through shared reason builder

Since the original PR, several new approval surfaces landed on main with
their own terse denial strings (CLI connectors, ACP permissions, Cline Hub
webview, desktop webview, example VS Code extension). Route all of them
through buildUserRejectedToolReason so the model sees a consistent,
non-error rejection message.

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

* Add buildUserRejectedToolReason to the @cline/shared integration-test stub

The VS Code integration tests run the tsc-built CJS tree and stub the
ESM-only @cline/shared package in test-setup.js; the stub was missing the
new export, so tool-approval-denial.js threw at module load in CI.

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

* Trim scope back to the minimal rejection-copy fix

Restore the connector deniedReason plumbing, ACP permission strings,
desktop webview reason, example extension reason, and hub server fallback
to their main versions. Those surfaces already attribute the denial to a
user and are outside ENG-2329. Keep the Cline Hub webview change since
that path emits its own rejection string the model sees.

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

* Move rejection guidance suffix into agent runtime per review

* Apply review suggestions: neutral fallback reason and -- separator before rejection suffix

---------

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

* Default web search on for the desktop app (#13725)

* Default web search on for the desktop app

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

* Make desktop web search default seed best-effort

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

---------

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

* fix(desktop): reconcile experimental sync behavior

* fix(desktop): enable macOS voice input (#13741)

* Promote ClinePass across home banner, account page, and settings (#12556)

* feat(webview): promote ClinePass across home banner, account page, and settings

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

* fix(webview): drop removed ext-cline-pass flag gating and hardcoded pricing from ClinePass promos

The ext-cline-pass feature flag no longer exists (the provider is ungated on
main), so promo surfaces are now gated only on self-hosted mode and org
remote-config provider allowlists. Promo copy describes the subscription
without a hardcoded price, matching the CLI copy cleanup in #13514.

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

* fix(webview): open the personal dashboard context from ClinePass subscription links

ClinePass always bills the personal account, but the Manage Subscription
button (and the ClinePass provider's usage link) landed org-context users
on the org dashboard. Pass personal=true like EntitlementError and the
CLI subscription links already do.

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

---------

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

* Desktop marketplace: show detail panel only on item click, left-align detail content (#13747)

* Desktop marketplace: show detail panel only on click, left-align detail content

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

* Desktop marketplace: drop license cell, single Learn more link (homepage, else repo)

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

* Desktop marketplace: keep selected entry open while list is filtered

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

---------

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

* feat(desktop): import sessions from Claude Code, Codex, and opencode (#13744)

* feat(core): session import service for Claude Code, Codex, and opencode history

Adds a SessionImportService to @cline/core that discovers sessions in the
on-disk stores of Claude Code (~/.claude/projects JSONL), Codex
(~/.codex/sessions rollouts + session_index titles), and opencode
(opencode.db sqlite), translates each conversation into Cline's native
MessageWithMetadata format, and persists it through CoreSessionService as
a completed, listable, resumable session.

Key mechanics:
- Claude Code: parentUuid tree walk from the newest leaf picks the active
  branch (edits/retries branch the log); same-message.id assistant lines
  merge back into one turn; sidechains, meta lines, and slash-command
  wrappers are excluded; ai-title/summary lines provide titles.
- Codex: real prompts come from user_message event_msg lines (user-role
  response_items are injected AGENTS/environment context, with a fallback
  for old rollouts); function_call/output pairs map to tool_use/tool_result;
  resumed rollouts that re-embed the original session id dedupe to the
  richest file; token_count events stamp per-turn metrics.
- opencode: reads a temp snapshot of the WAL-mode db; inline tool parts
  split into tool_use + tool_result to preserve provider-valid structure;
  child (subagent) sessions and synthetic parts are skipped.
- Shared sanitizer guarantees replayability: orphaned tool_use gets a
  placeholder result, orphaned tool_results and empty text blocks drop,
  provider-session-scoped signatures/encrypted reasoning strip.
- Imported sessions pass every history-visibility gate (terminal status,
  non-empty provider/model, chat-workspace fallback cwd, no fabricated
  checkpoint metadata) and carry metadata.importedFrom for idempotent
  re-discovery (alreadyImportedSessionId).

* feat(desktop): sidecar commands for importing sessions from other tools

Adds two sidecar WebSocket commands backed by @cline/core's
SessionImportService:

- list_importable_sessions: returns { installedTools, sessions } where
  sessions are ImportableSessionSummary rows (tool, sourceId, title, cwd,
  timestamps, messageCount, preview, alreadyImportedSessionId) discovered
  in the local Claude Code / Codex / opencode stores.
- import_sessions: takes { selections: [{ tool, sourceId }] }, validates
  each selection against the known tool list, imports sequentially
  (per-session transactional), and broadcasts session_import_progress
  events ({ index, total, result }) so the UI can render live progress.
  Returns { results } with per-item ok/sessionId/title/error.

* feat(desktop): import sessions UI for Claude Code, Codex, and opencode

Adds an Import Sessions dialog to the desktop app driven by the sidecar's
list_importable_sessions / import_sessions commands:

- Scan phase discovers local history from all three tools and groups it
  per tool with select-all checkboxes, per-row title, relative time,
  message count, and workspace folder; rows already imported are disabled
  and badged (idempotent re-open).
- Text filter across title, folder, and first-prompt preview.
- Import phase streams session_import_progress events into a progress bar
  and per-item result list; the dialog cannot be dismissed mid-import via
  overlay click. Done phase summarizes successes and lists failures with
  their error messages.
- Entry points: an Import button in the Sessions view header and an
  "Import sessions" row in Settings → General.
- use-session-history subscribes to session_import_progress so history
  refreshes no matter which surface started the import.
- Wire types live in webview/lib/session-import.ts (mirrors the core
  module's types so the client bundle never imports node-only code).

* fix(desktop): import dialog crash rendering session timestamps

formatRelativeTime takes a string (parseTimestamp calls .trim() on any
truthy value), but the import dialog passed the numeric updatedAtMs,
crashing the page with 'e.trim is not a function' as soon as scanned rows
rendered. Convert to an ISO string at the call site.

Slipped through because the webview has no typechecking anywhere:
tsconfig.dev.json excludes webview/ and next.config sets
typescript.ignoreBuildErrors, and the webview's own tsconfig currently
carries 64 pre-existing errors.

* feat(desktop): offer session import during onboarding

Adds an 'import' onboarding step between connect/github and done. The
step scans for importable Claude Code / Codex / opencode history on
entry and silently advances when nothing (new) is found or the scan
fails, so only people with actual history from other tools ever see it.
When sessions are found it summarizes the count and source tools, opens
the same ImportSessionsDialog used by the Sessions page for picking, and
flips to a confirmation state once at least one session imports. Skip is
always available, including while the scan is still running.

* fix(desktop): import dialog text overflow, collapsible sections, select all

- Titles no longer clip or push the row wide: they word-wrap up to two
  lines (line-clamp-2 + break-words, with min-w-0 down the flex chain so
  long unbroken Codex prompt titles can actually shrink); the meta line
  keeps time/count fixed and truncates only the workspace name; progress
  rows get the same min-w-0 treatment.
- Each tool section header is now a collapse toggle (chevron +
  aria-expanded) so one tool with hundreds of sessions doesn't force
  scrolling past it; collapsed headers still show count and selected
  count, and filtering forces sections open so search matches can't hide
  in a collapsed group. Collapse state resets per dialog open.
- New global Select all row above the list with indeterminate state and
  an x-of-y selected counter; it operates on the currently visible
  (filtered) selectable sessions, matching the per-section checkboxes.

* fix(desktop): import dialog header and search clipped by intrinsic column width

The dialog grid used the default auto column track, so a single
unbreakable string in a session title (Codex titles often contain URLs)
set the column's min-content width wider than the fixed 620px dialog --
break-words affects layout but not intrinsic sizing -- and
overflow-hidden then clipped everything in the column, including the
description and the search field. Pin the column to minmax(0,1fr) so the
container width always wins and long words wrap at the box edge instead.

Also add sm:max-w-none (the primitive's sm:max-w-lg survives
tailwind-merge across variants and was silently capping the dialog at
512px) and shrink-0 on the search and select-all rows so a tall list can
never compress them vertically.

* fix(desktop): onboarding import step rescanned after import and looped to done screen

The import step's scan effect depended on onContinue, an inline arrow the
parent recreates every render — and importing itself re-renders the app
shell via the history refresh. Each re-render re-ran the scan, and when
the user had imported everything (select all), the re-scan found zero
remaining sessions and hit the nothing-to-import auto-advance, yanking
them past their own import confirmation onto the done screen. The scan
now runs exactly once per step entry (onContinue held in a ref for the
async auto-skip paths).

Also, after a successful import the button is now 'Start building' and
completes onboarding directly instead of routing through the separate
done screen — two consecutive confirmation screens read as a loop. The
skip and nothing-found paths still go through the done screen so those
users get the 'You're all set' confirmation.

* fix(core): consolidate imported tool_results into the message after their tool_use

The import sanitizer answered missing tool_use ids with a separate
placeholder user message while leaving real results for the same turn in
later user messages. Anthropic requires every tool_result for a turn in
the user message immediately following it, so a partially-answered turn
would still 400 on resume. Rebuild any incomplete or split span as one
consolidated results message in tool_use order (placeholders for missing
ids, duplicates dropped) followed by a message carrying whatever else the
span held, mirroring the legacy migration sanitizer.

* fix(desktop): imported sessions resume on the user's configured provider; batch adapter caches

Opening a history session adopts the row's provider/model
(use-chat-session: session.provider || prev.provider), so imported rows
stamped with the source tool's provider — openai-native for Codex,
whatever opencode reported — resumed on providers the user may never have
configured and failed on first send. The dialog now passes the app's
current model selection (lastProvider/lastModelByProvider, i.e. what a
new chat would run on) and the service stamps it on the row; both halves
must be present so a Cline provider is never paired with a foreign model
id. The source provider/model are preserved in metadata.importedFrom and
per-message modelInfo stays accurate. Codex's provider id is corrected to
Cline's openai-native, and opencode's openai/google map to
openai-native/gemini.

Adapters also gain per-batch caches released via dispose(): Codex's
convert() re-walked the sessions tree and re-read every rollout head per
imported session (O(sessions x files)); it now builds the session-id ->
richest-file index once per batch. opencode copied the whole WAL db per
imported session; it now snapshots once per batch.

* fix(core): roll back failed imports and dedupe at import time

Addresses both Greptile P1s on #13744:

- A write failing after createRootSessionWithArtifacts (messages, status,
  manifest, title) left a half-written pid-0 session in history whose
  importedFrom marker also blocked retrying the source. persistConverted
  now deletes the session on any later failure and rethrows.
- Dedup markers were read through listSessions, which caps its scan at
  2000 rows, so a prior import older than the newest 2000 sessions was
  invisible and the source could be imported again. Add
  listSessionMetadata (ids + metadata for every row, no manifest reads or
  reconciliation) and use it for markers. Also check idempotency at
  import time, not only at discovery: a request for an already-imported
  source resolves to the existing session (alreadyImported: true) instead
  of writing a copy, covering stale pickers and repeated requests.

* fix(core): create imported sessions terminal and mark them imported last

Two failure modes shared one root cause -- the import wrote its session
in stages and claimed success too early:

- The row was created running/pid-0 and flipped to completed afterwards.
  The stale-session reconciler runs in the hub daemon against the same
  SQLite DB and, in that window, marks such rows failed and stamps
  terminal_marker metadata. createRootSessionWithArtifacts now accepts
  status/endedAt/exitCode so imports are created completed with the
  source session's end time; the separate status flip and manifest
  rewrite are gone.
- The importedFrom marker was written at creation, so a session whose
  later writes failed (and whose rollback delete also failed) still
  blocked retrying its source. The marker is now the final write, so it
  means 'this import finished' and a half-written session can never
  claim the source.

listSessionMetadata is unbounded by default so dedup sees every row.

* fix(core): resolve TS2352 casts in session-import tests (#13746)

tsc rejects casting ContentBlock[] straight to Record<string, unknown>[]
(RedactedThinkingContent is not comparable), which failed the Quality
Checks typecheck. Route the five assertion-site casts through a small
blocks() helper that widens via unknown.

* fix(core): flatten Codex content-block tool outputs during import

Newer Codex rollouts write custom_tool_call_output.output as an array of
Responses-API content blocks ({type:"input_text", text}) instead of a
plain string. The importer JSON.stringified that array into the
tool_result content, and the chat UI's tool-summary parser then rendered
each non-text block as its type label, so imported exec calls showed up
as "[input_text][input_text]" with no output.

Concatenate the text of string/text-bearing blocks (they are stream
chunks, so no separator) and keep the JSON fallback for anything else.

* fix(desktop): edit-and-resend on runs without a checkpoint

Editing a message forks the session before that run, and the sidecar
always routed that through manager.restore with workspace: true. Imported
sessions carry no checkpoint history, so editing any of their prompts
failed with "No checkpoint found at or before run N" — even after the
user had continued the session in Cline, since only the new runs get
checkpoints.

When no checkpoint exists at or before the edited run there is no
workspace state to roll back, so fork the trimmed transcript onto the
current workspace (the same path a full-history fork takes) instead of
erroring. Runs that do have a checkpoint still restore the workspace.

* fix(core): roll back failed session creation and coalesce overlapping imports

Two gaps Greptile flagged on the import path:

createRootSessionWithArtifacts upserts the row before writing the messages
file and manifest, and the call sat above persistConverted's rollback try.
A file write failing there left a completed row with no transcript in
history. Creation now runs inside the rollback, and deleteSession already
tolerates a missing row or missing files.

Each import_sessions request builds its own service and snapshots the
existing-import markers once, so two overlapping requests for one source
(a second window, a double-fired command) both passed the dedupe check and
persisted two sessions. A module-level in-flight map keyed by tool:sourceId
makes the later caller wait on the first write and report its session as
already imported.

* fix(desktop): resolve the import resume target like a new chat does

An imported Claude Code session resumed on the Anthropic provider instead
of the user's Cline selection. The dialog read model-selection storage
directly and required both a remembered provider and a remembered model;
the composer only records a model from the explicit picker handlers, so
anyone running on the default model has no entry, the lookup came back
empty, and the service fell back to the source tool's provider.

Resolve the target with getInitialChatConfig() -- the same chain a new
chat uses (remembered selection, then the built-in default), which is
never empty -- and have the import_sessions handler default to the cline
provider and CLINE_DEFAULT_MODEL_ID when a caller sends nothing, matching
other server-started sessions. The source provider can no longer become
the resume target.

* feat(desktop): group scheduled runs under their schedule in the sidebar (#13752)

* feat(core): stamp schedule id, name, and run number onto scheduled sessions

Sessions started by the cron runner only carried a generic
sessionHistoryOrigin.trigger = "hub-schedule", so clients could tell a
session was scheduled but not which schedule it belonged to or which run
it was. The runner now passes schedule provenance to the runtime handlers,
which merge it into the session metadata alongside the origin trigger:

  scheduleId          the hub schedule's external id
  scheduleName        the schedule title
  scheduleExecutionId the cron run id
  scheduleRunNumber   1-based position among every run created for the spec

The run number comes from a new SqliteCronStore.getRunOrdinal, which counts
runs of every status in creation order so a later cancellation never shifts
numbers already stamped onto earlier sessions. A reclaimed run keeps its
number, so two sessions with the same number make a duplicate visible.

HubScheduleRuntimeHandlers.startSession gains an optional second argument
carrying the metadata; existing implementations that ignore it keep working.

* feat(desktop): group scheduled runs under their schedule in the sidebar

A schedule that fires daily filled the sidebar's Scheduled section with a
row per run, each titled with the same prompt text, which read as if the
task had been duplicated. Runs of one schedule now fold into a single
collapsible row named after the schedule, with the run count on the right;
expanding it lists the runs as "Run N" sub-items (newest first) with their
usual status dot, time, hover card, context menu, and delete button. The
group holding the active session expands on its own so a run opened from
the Schedules page is visible. Grouping also applies inside project groups
when sorting by project. The Scheduled header now counts schedules rather
than runs.

Threads learn the schedule identity from the metadata the runner now
stamps (scheduleId, scheduleName, scheduleRunNumber). Runs recorded before
that fall back to the schedule executions list the hook already polls,
which now yields the schedule id and name instead of a bare session id set,
and finally to grouping by shared title. Runs without a number are labelled
with their start time instead of "Run N".

* fix(desktop): reopen a collapsed schedule group when one of its runs is opened

A stored collapse used to win over the active-session default for the
sidebar's lifetime, so a run opened from the Schedules settings page
could stay hidden inside its collapsed group. Opening a session now
clears the stored choice for the group that holds it; the group can
still be collapsed afterwards.

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

---------

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

* feat(cli): handle outdated hub sessions with drain and replace flow (#13727)

* feat(cli): handle outdated hub sessions with drain and replace flow

Add logic to detect when the CLI is newer than the running Hub and provide
users with options to either keep the older Hub running (to avoid
interrupting active sessions from other clients) or force-replace it.

Implement `describeOutdatedHubSessions` helper to show quantified session
activity in the dialog, and add `HubOutdatedContent` UI component with
detailed messaging for the `build_mismatch` case. The `unsupported_protocol`
case remains a modal requiring update, while the softer mismatch now uses
a toast with enter-to-replace or escape-to-keep choices.

Includes tests for draining and replacing an older busy hub when forced.

* fix(hub): gate desktop hub_upgrade behind trusted connection and make drain-first a hard guarantee

Address review: an originless local WebSocket client could invoke the
forceful hub_upgrade command, and a failed drain request still allowed a
forced retirement, so work started during the wait window could be killed.

- hub_upgrade now requires the same canApproveTools per-connection gate as
  the tool-approval commands.
- upgradeManagedHub skips the idle-wait window when the drain was not
  established (an undrained hub keeps admitting work, so waiting only
  widens the blast radius) and refuses to replace a busy hub that did not
  accept the drain, force or not. An idle hub is still replaced so
  pre-drain-endpoint hubs (404) remain upgradable.

* fix(hub): treat failed activity readings as unknown, not idle, during hub upgrade

A transient session.list failure inside the drain wait window previously
read as an idle hub, which could end the grace window early and authorize
retirement while turns were still finishing.

- Failed readings never end the wait window early, never overwrite the
  last real observation, and never authorize a non-forced retirement.
- Without force, a hub whose activity was never confirmed is handed back
  un-drained (still_busy) instead of retired; an undrained hub is now
  replaced only when positively observed idle.
- With force and an accepted drain, an unanswerable hub is still replaced:
  the user already consented to interrupting its sessions.

* fix(hub): never retire an undrained hub on an idle snapshot

An older hub that rejects the drain has no admission barrier, so a single
idle reading cannot authorize retirement: a session admitted right after
the snapshot would die in a retire the consent prompt never covered.

upgradeManagedHub now retires a hub only under an accepted drain. The
undrained-idle case is delegated to the locked ensure path, which
re-checks activity immediately before its own retire ladder and attaches
(deferring the swap) when new work arrived in the meantime; the upgrade
then reports still_busy instead of replaced, and the desktop/TUI surfaces
tell the user to retry.

* fix(hub): require an accepted drain unconditionally before any upgrade retirement

Review follow-up: the undrained-idle delegation still reached
retireDiscoveredHub, whose own drain attempt is best-effort, so a session
admitted after the idle re-check could die in the shutdown.

upgradeManagedHub now fails fast when the hub does not accept the drain -
no wait window, no idle exception, no delegation. The drain is the
admission barrier that keeps every subsequent reading true through the
retire; a hub too old or wedged to accept it is left to the automatic
ensure path, which replaces it once idle at the next client startup, and
the error says so.

* fix(hub): establish the drain barrier before the automatic idle check

Review follow-up: the automatic incompatible-hub path read session
activity first and drained only inside the retire ladder, so a session
admitted between the idle snapshot and the shutdown could be terminated.

retireIncompatibleHub now requests the drain before the busy check: with
the drain accepted, the idle reading stays true through the retire. A
deferred (busy) hub, and one whose retirement fails or is skipped by the
circuit breaker, gets the drain lifted so it never sits alive-but-refusing
work. Hubs that do not accept the drain (pre-/drain builds answer 404)
keep the historical best-effort snapshot rather than being stranded
forever.

* polish(hub): tighten the outdated-hub dialog copy

Two short sentences instead of four long ones, spell out what Quit Cline
does (closes the app, leaves the Hub running), and rename the action to
Update Now in both the desktop dialog and the TUI variant.

* fix(cli): show the keep-Hub reminder toast when the outdated-hub dialog is dismissed (#13754)

dialog.choice() resolves undefined on Esc rather than rejecting, so the
reminder toast in .catch() never ran. Move it to the falsy branch of
.then(), matching the unsupported_protocol handler.

---------

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

* chore(sdk): release v0.0.82

* chore(cli): release v3.0.61

* fix(core): close imported-session stores before the temp dirs are removed

The session-import tests opened a SqliteSessionStore per case and never
closed it, so afterEach's rmSync ran against a directory still holding an
open SQLite file. POSIX allows that; Windows does not, and all seven
persisting cases failed the sdk-publish Windows job with EPERM on the
cline-db-* temp dir.

Route every store through a sessionStore() helper that registers it for
close, and close them before removing the dirs.

* chore(vscode): release v4.1.17 (#13755)

* chore(desktop): release v0.0.22

* fix(standalone): decode core-connection protobus requests from proto3 JSON (#13758)

The core connection delivers protobus requests as the proto3 JSON the
webview's ts-proto toJSON encoders produce: enums arrive as string names
and default-valued fields — empty repeated fields included — are omitted.
The handlers assume ts-proto message shapes (numeric enums, repeated
fields always present), so dispatching the parsed JSON directly broke
every RPC relying on those invariants on JetBrains: changing the API
provider threw 'Cannot read properties of undefined (reading length)'
in fromProtobufModelInfo, and the plan/act toggle rejected its own mode
as invalid. The old standalone gRPC server restored these defaults
during protobuf decoding; the tunnel skipped that step.

Generate a per-method request-decoder map (request type fromJSON)
alongside the service handlers and apply it in the core-connection
dispatcher before dispatch. The in-process VS Code webview path is
untouched: it posts structured-cloned ts-proto objects that never pass
through JSON.

* feat(core): Hub-managed Agent Plugins support (#13652)

* feat(sdk): add hub-managed Agent Plugins

* fix(sdk): restrict Agent Plugin auto-discovery

* test(sdk): canonicalize Windows plugin paths

* fix(sdk): await stdio MCP process shutdown

* fix(sdk): select Agent Plugin MCP clients by source

* fix(core): defer Agent Plugin data directory creation

* docs(sdk): clarify Agent Plugin discovery scope

* fix(sdk): reject individual Agent Plugin skill toggles

settings.toggle({type: "skills"}) unconditionally called
toggleSkillFrontmatter() for any resolved skill record, including ones
sourced from an Agent Plugin. That writes a `disabled` key into the
skill's SKILL.md frontmatter, but the strict Agent Skills parser used
for these skills only permits a closed field set (name, description,
license, compatibility, metadata, allowed-tools). The very next reload
then rejects the file as invalid and the skill silently disappears
until someone hand-edits the installed plugin's SKILL.md.

Guard the toggle: an agent-plugin-sourced skill record now throws a
clear error pointing at the plugin-level toggle instead, matching how
whole-plugin enable/disable already works (setDisabledAgentPlugin,
keyed by manifest name, no file mutation).

* fix(sdk): keep disposing MCP servers when one disconnect fails

InMemoryMcpManager.dispose() unregistered servers sequentially and let
the first disconnect() rejection abort the loop. Since disconnect() can
now reject when a stdio child never exits, one wedged server would leak
every remaining server's process. Catch per-server errors, disconnect
the rest, and rethrow as an AggregateError so upstream cleanup-error
reporting still sees the failure.

Also log agent plugin discovery failures in CoreSettingsService.list
instead of swallowing them silently, so a plugin missing from settings
is diagnosable.

* feat(cli): manage Agent Plugins through the Hub (#13657)

* feat(desktop): manage Agent Plugins through the Hub (#13658)

* feat(desktop): manage Agent Plugins through the Hub

* fix(desktop): show Agent Plugin inventory

* docs: add deprecation notices page (#13458)

* docs: add deprecation notices page

* docs: add primary surface to deprecations

* chore: drop unrelated formatting changes from docs PR

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>

* fix(desktop): harden main sync integrations

* style: normalize sync conflict formatting

* revert: preserve repository formatter conventions

* style: satisfy VS Code merge formatting

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: JasmineLCY <38378321+JasmineLCY@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Haley Park <haleypark.design@gmail.com>
Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>
Co-authored-by: yzxcj797 <54314860+yzxcj797@users.noreply.github.com>
Co-authored-by: yzxcj797 <yzxcj797@users.noreply.github.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: 𝓜𝓲𝓼𝓼𝓪𝓻𝓲 𝓐𝓱𝓲𝓵 🌿 <143264692+missarii@users.noreply.github.com>
Co-authored-by: Dominic Cooney <dominic.cooney@cline.bot>
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Harrison <harrison@cline.bot>
Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: TheRealSpencer <32678829+TheRealSpencer@users.noreply.github.com>
Co-authored-by: Etisha Garg <etisha.garg@cline.bot>
2026-09-02 16:27:24 -07:00
John Choi db9d3a3436 fix(desktop): open GitHub install from cloud onboarding (#13779)
* fix(desktop): open GitHub install from cloud onboarding

* fix(desktop): align cloud GitHub connect actions
2026-09-02 16:17:22 -07:00
BeeandCursor Agent 1e391caa39 feat(desktop): port configurable media generation to desktop-experimental (#13778)
* feat(tools): add configurable media generation

* fix(desktop): validate media tool selections

* fix(desktop): gate media toggle during catalog refresh

* test(desktop): align chat-model expectations with merged isChatModel semantics

* fix(desktop): reconcile media generation with experimental updates

* fix(media): clarify omitted generated image visibility

* bun run build:models

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-09-02 13:45:47 -07:00
+14 6b406219b2 chore(desktop): bump beta to 0.0.22-beta.1 (#13743)
* fix(vscode): continue the surviving session on resume instead of rebuilding with the original task text (#13175)

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilt the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

The preserved conversation history is the source of truth on resume, so
the fallback prompt now just asks the model to reassess the history and
continue, matching the legacy resume prompt which also never resent the
original task. User-typed text still takes precedence when provided.

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

* fix(vscode): continue the surviving idle session on follow-ups instead of rebuilding

Stopping a turn keeps the session alive, but every idle follow-up (bare
Resume after Stop, and typed follow-ups after a completed turn) tore
that session down and rebuilt it from persisted task history before
sending. Continue the matching idle session in place instead, the same
way the CLI reuses the live session after an abort. Rebuilding from
history now only happens when no live session matches the displayed
task (task opened from history, extension host reload).

A bare resume still needs a prompt to start a turn, so it sends the
neutral [TASK RESUMPTION] prompt (shared with the rebuild fallback and
hidden from the transcript); user-typed content is echoed and sent
as-is. If the send lands while the abort is still settling, the runtime
auto-queues it and drains once the abort completes.

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

* refactor(vscode): consolidate follow-up send paths in SdkFollowupCoordinator

Now that idle follow-ups continue the live session in place, the
two-mode sendToActiveSession helper was redundant: its non-queued branch
duplicated continueIdleSession minus the bare-resume prompt. Split it
into a single-purpose queueToActiveSession and fold the idle no-task
send into continueIdleSession, flattening askResponse's decision tree
to: queue onto a running turn, continue a matching live idle session,
rebuild from history, or abandon.

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

* refactor(vscode): reuse the existing neutral resumption prompt for bare resumes

Drop the newly invented long resumption wording in favor of the phrase
that already existed as the no-history fallback and that the transcript
hiding logic and test fixtures recognize: '[TASK RESUMPTION] Please
continue where you left off.' The net change to resumeSessionFromTask
against main is now just deleting the branch that resubmitted
historyItem.task as new instructions.

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

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilds the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

Bare resumes now always use the neutral prompt that already existed as
the no-history fallback; user-typed text still takes precedence. This
matches the legacy resume prompt (responses.taskResumption), which only
ever included user-supplied text as new instructions.

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

* fix(vscode): hide synthetic prompts from the queued-prompt echo

A send that races a settling abort is auto-queued by the runtime, so a
bare Resume can reach the pending_prompt_submitted echo carrying the
synthetic [TASK RESUMPTION] prompt. Echoing it leaked model-facing text
as a visible user bubble and shifted the visible-user-message ordinals
that edit/regenerate mapping relies on. Filter synthetic prompts with
isSyntheticUserPrompt, keeping user attachments visible (matching
isSyntheticSdkUserMessage semantics).

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

---------

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

* fix(vscode): preserve LiteLLM input token limits (#13293)

* fix(vscode): preserve LiteLLM input token limits

* fix(vscode): prefer live LiteLLM model metadata

* fix(vscode): generalize private catalog metadata

* test(vscode): preserve llms exports in vscode lm mock

* fix(vscode): point provider signup URLs at their API key pages (#13337)

* fix(vscode): point Mistral signup URL at the general API keys console

The Mistral provider's signup link led to the Codestral console, which
issues Codestral-scoped keys that fail with 401 on api.mistral.ai — the
endpoint the provider actually calls. Point it at the general API keys
page instead.

Fixes #13288

* fix(vscode): deep-link DeepSeek and Fireworks signup URLs to their API key pages

Both pointed at marketing homepages; link straight to the key-creation
pages instead, matching the rest of the registry and the desktop app's
provider-key-urls map.

* fix(ci): always build the legacy bundle from the legacy-extension branch (#13349)

The combined-VSIX workflow took legacy-ref as a free-form dispatch input
with no publish-time validation (next-ref has one: publish requires main).
Any typed ref — a PR merge ref, an unprotected branch — would be built
into the published VSIX by the environment-less build job, and the publish
environment approver only ever sees an opaque prebuilt artifact, so the
approval protected the marketplace PAT but not the shipped bytes.

Remove the input entirely and hardcode the protected legacy-extension
branch, which makes that branch's protection rules load-bearing for
releases. The tested-sha pinning between test-legacy and build is
unchanged. publish-extension skill dispatch command updated to match.

* fix(ci): lock the legacy publish workflow to the legacy-extension branch (#13350)

The branch dispatch input was a free-form string with no validation. Both
jobs checked it out and ran full npm lifecycle scripts from it: the publish
job next to VSCE_PAT/OVSX_PAT (and npm run publish:marketplace executes a
script from that same ref with the PATs in env), and the test job with NO
environment approval at all while inheriting the workflow-level
contents/packages/checks/pull-requests write grants. A dispatch pointing at
e.g. refs/pull/N/head would run outside-contributor code with the
marketplace keys behind one approval, or with a repo-write token behind
none.

Remove the input and hardcode the protected legacy-extension branch, drop
the workflow-level permissions to contents: read, and elevate only the
publish job to contents: write (tag push + GitHub release). The branch
input's default was legacy-extension, so normal publishes are unchanged.
publish-extension skill dispatch command updated to match.

* fix(vscode): SDK remote-config parity — refresh coordination, session gating, and fail-closed opt-out (#13226)

* feat(desktop): native notifications (#13166)

* feat(desktop): native notifications

* macos target

* fix(desktop): isolate macOS dev app identity

* fix(desktop): address notification review feedback

---------

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

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched (#13310)

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched

Toggling an auto-approve setting while a task is open writes
autoApprovalSettings into the StateManager's task-settings overlay
(updateAutoApprovalSettings -> setTaskSettings). The SDK controller never
cleared that overlay on clearTask/showTaskWithId (the legacy controller
did), so after New Task the stale overlay kept shadowing global settings
in getGlobalSettingsKey(): toggle RPCs were accepted into global state,
but every posted state still carried the overlay's old version, which the
webview rejects as not newer - the auto-approve checkboxes froze forever.

Restore legacy parity in SdkTaskControlCoordinator: drop the overlay
(persisting pending writes first) in clearTask() and before installing a
different task's proxy in showTaskWithId().

Fixes #13260

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

* changeset

* test(vscode): add end-to-end regression test for auto-approve freeze after New Task

Wires the real StateManager, the real updateAutoApprovalSettings handler,
and the real SdkTaskControlCoordinator.clearTask() together with the
webview's version gate modeled on ExtensionStateContext, pinning the
end-to-end invariant behind #13260: checkbox toggles must keep reaching
the webview after a mid-task toggle followed by New Task. Verified the
test fails when the clearTaskSettings() call is removed from clearTask().

* fix implicit any in regression test

---------

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

* feat(desktop): show provider web-search support under the settings toggle (#13328)

* feat(desktop): show provider web-search support under the settings toggle

The global Web search toggle silently does nothing unless the session's
provider offers native web search, which made the setting read as if it
worked with any provider. The desktop General settings row now explains
that only providers with built-in web search honor it, and shows a live
status line: which connected providers are ready to use it (no extra
setup needed), or an amber warning with a link to the Models section
when none of them support it.

Support is resolved in the webview via a new providerOffersModelTool
helper in @cline/llms (browser export), sharing the same builtin-manifest
source of truth as the runtime's supportsModelTool attachment check.

* fix(desktop): address review — refetch web-search status on catalog invalidation, clarify per-model support

Greptile P2: the one-time catalog fetch could race an in-flight provider
save and show stale status; the row now refetches when the provider
catalog cache is invalidated (fired after saves complete).

Greptile P1: the ready line implied every model on the provider works;
Vertex excludes Claude routes, so the copy now scopes the promise to
models that support it.

---------

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

* feat(ui/desktop): collapse finished runs into a work summary and remove hover-state dead space (#13315)

* feat(ui): add WorkActivity collapsed-run summary and float message actions as a pill

WorkActivity/WorkActivityTrigger/WorkActivityContent fold a finished agent
run's working rows (tool calls, thinking traces, narration) behind a single
"Worked for 4m 12s · 14 tool calls" disclosure built on the shared animated
disclosure primitives, with formatWorkActivityLabel/formatWorkDuration
exported for consumers.

Message hover actions no longer rely on the transcript reserving blank space
below each message: the action row is now a self-backed pill (border,
blurred background, shadow) that floats over whatever follows, so
conversations can pack rows tightly without hover chrome colliding with the
next message.

* feat(desktop): collapse finished runs into a work summary and tighten chat spacing

collapseCompletedWork post-processes the grouped transcript: once a run ends
on assistant text with no further tool calls, its working rows fold into one
expandable WorkActivity row while the final answer stays visible. Runs are
delimited by user messages; the trailing run only collapses when the session
has stopped running and actually produced an answer, so live streams and
cancelled/failed tails keep their rows. Assistant messages carrying images
or media are treated as deliverables and never collapse.

The conversation list gap drops from gap-8 to gap-4 now that hover actions
are self-backed pills that need no reserved space, and user messages add
their own top margin so turn boundaries stay visually distinct.

* refactor(ui/desktop): work summary label wording, flat expansion, stable in-run rhythm

Feedback round on #13315:

- Label reads "Worked for 4m 12s and made 14 tool calls" instead of joining
  with a dot; without a duration it falls back to "Made N tool calls".
- Expanded work rows render at transcript level — no rail or extra indent —
  since tool rows and thinking traces already carry their own nesting when
  expanded. The work content keeps the tight working-row rhythm.
- Live working rows (thinking traces + tool calls) now group into a 'run'
  render item with the same tight 0.25rem rhythm, so there is no oversized
  gap under a "Thought for Ns" row and every row keeps its exact position
  when the finished run folds into the work summary. A trailing
  answer-in-progress stays outside the group at transcript level, and pure
  prose spans keep normal spacing.
- The transient "Thinking..." indicator moves inside the transcript column
  and mirrors a trigger row's geometry, so the first real row replaces it in
  place with no jump.

* style(ui/desktop): hover-pill metrics, right-pointing work chevron, scroll and spacing fixes

Another feedback round on #13315:

- Hover action pill: +2px internal padding, a trailing inset after the
  timestamp (it sat flush against the pill border), and more clearance
  between the message content and the pill (2px -> 6px; the hover bridge
  grows to match).
- The work summary chevron points right while collapsed and continues
  counterclockwise to point up when expanded.
- Conversation bottom padding drops pb-20 -> pb-8: the composer sits below
  the scroller, so the padding only needs to clear a pinned action pill.
- Sending a message scrolls back to the bottom even if the reader had
  scrolled up (new AutoScrollOnSend on the user-message count, which ignores
  optimistic-bubble re-keying; @cline/ui now exports useConversation for
  this).
- An assistant answer directly under its run's working rows pulls itself
  0.5rem closer than the full transcript gap.

* style(desktop): leave a visible gap between a pinned action pill and the composer

pb-8 exactly matched the pill's ~40px footprint, so the last row's hover
actions sat flush against the composer top; pb-12 restores ~8px of daylight.

* style(desktop): widen the gap between the pinned action pill and the composer to ~24px

pb-12 left only ~8px of daylight under the pill; pb-16 reads comfortable
without reverting to pb-20's dead space.

* fix(desktop): keep the thinking indicator at the working-row offset mid-run

The indicator matched a trigger row's geometry but sat a full transcript gap
(1rem) below the last working row, while the tool/thinking row replacing it
joins the tight run group at 0.25rem — a visible upward jump. When the last
transcript item is working rows (or streamed assistant output), the
indicator now pulls up to the same tight offset; only at the start of a run,
under the user message, does it keep the normal gap.

* style(ui): calm the hover actions surface per team feedback

Borderless rectangle instead of the bordered pill: radius drops to
var(--radius), the side padding goes entirely (the icon buttons carry their
own hit areas), and the vertical padding halves. Blurred background and
shadow stay so it remains legible over following content.

* feat(ui/desktop): full-band hover reveal and iOS-style disclosure easing

The hover actions only appeared while the pointer was inside the message
box itself. The invisible bridge under each message now spans the full
height of the band the floating actions occupy (full row width), so
hovering anywhere in that strip reveals them. Sibling row types
(.cline-chat-tool, .cline-chat-work, and the desktop's run/tool groups)
become position: relative so they paint above the bridge — their own
content keeps its hover and clicks, and the bridge only wins in the band's
genuinely empty space.

All expandable rows (work summary, tool panels, thinking) open and close on
a 240ms symmetric ease-in-out cubic-bezier instead of the 60ms snap, with
chevron rotation on the same curve. Reduced-motion still disables both.

* revert(ui/desktop): drop the full-band hover reveal; quicken disclosure easing to 180ms

The full-band hover bridge (and the position: relative changes that made it
safe) is reverted per feedback — back to the narrow bridge that only spans
the gap under the message. The iOS-style ease-in-out on disclosures stays
but speeds up from 240ms to 180ms.

* fix(ui): recover live tool diffs that mount as a blank pierre skeleton

Live-streamed edit rows could show an empty diff for the whole run, with the
diff only appearing after the collapsed work row was expanded (fresh mount).
Root cause, confirmed by driving a live session and inspecting the element:
React StrictMode double-invokes @pierre/diffs' ref callback; the first
instance's async highlight work aborts on its immediate cleanup, and the
second instance adopts the abandoned half-rendered shadow tree as if it were
complete prerendered output — zero height, no code, no theme stylesheet,
permanently. A rendered diff always carries style[data-theme-css] in its
shadow root, so ToolFileDiff now checks for it shortly after mount and
remounts FileDiff (bounded attempts) when missing; the fresh host element
takes the normal render path and recovers within ~400ms. Verified live: the
diff now renders during the run.

* fix(desktop): keep interrupted runs expanded even with partial trailing text

The trailing-run collapse gated on 'ended with assistant text', which
misread a Stop that landed mid-answer as a finished run and folded the tool
calls the user wants to inspect. The gate is now the terminal status itself:
only completed (or restored-idle) sessions collapse the trailing run;
cancelled/failed/error tails keep their rows regardless of partial text.
(Greptile P1 on #13315 — matches the PR's stated rule.)

* feat(ui): share the markdown pipeline, chat polish, and ThinkingBlock across products (#13323)

* feat(ui): share the markdown pipeline, chat polish CSS, and ThinkingBlock

The desktop app and the cloud dashboard both consume @cline/ui yet rendered
assistant output differently, because Markdown policy and the thinking-trace
row lived app-side. This moves the shareable parts into the package:

- components/markdown (new export): the lazy Shiki code highlighter (GitHub
  light/dark, pinned language set) and agentMarkdownControls — the standard
  Streamdown configuration. streamdown/shiki/@shikijs/* become optional peer
  dependencies, mirroring @pierre/diffs.
- components/markdown.css: the desktop's chat polish moves in — chat-scale
  headings, outside list markers, single quiet code blocks with a
  hover-revealed copy control, table cards. Kept unlayered so it beats
  Streamdown's layered Tailwind utilities without !important.
- ThinkingBlock + formatThoughtLabel in agent-chat: the standard thinking
  row (brain icon, Thinking/Thought-for-Ns label, streaming shimmer, rail
  presentation, capped scrollable body). The shimmer and the
  reasoning-hover-suppression rule move into agent-chat.css; triggers gain
  the color transition the desktop applied locally.

Version bumps to 0.2.0-next.5 for the dashboard to pick up.

* refactor(desktop): consume shared markdown and thinking primitives from @cline/ui

The local Shiki highlighter, Streamdown controls, chat markdown polish CSS,
streaming-title shimmer, and reasoning hover-suppression rule are deleted in
favor of the @cline/ui versions (the highlighter test moves to the package's
suite). ReasoningBlock becomes a thin wrapper that hands MemoizedMarkdown to
the shared ThinkingBlock, and formatThoughtLabel re-exports from the package
so grouping code and tests keep their import path.

globals.css now imports @cline/ui/components/markdown.css (unlayered, so the
polish keeps beating Streamdown's layered utilities); the app keeps only what
is genuinely app-specific: link/image policy in markdown.tsx, selectability
rules, accent palettes, and the view-enter transition.

* style(ui/desktop): make thinking-trace prose legible

Thinking body text rendered too faint: plain muted-foreground plus the
desktop's font-thin weight. The shared thinking content now leans 75% of the
way back toward the body text color (still slightly de-emphasized), and the
desktop drops the thin font weight.

* ci(ui-publish): build @cline/shared before ui typecheck (#13354)

@cline/ui's generated-media imports @cline/shared/browser, which resolves to
shared's dist output. The build-shared step sat after typecheck/test/build,
so the first ui-publish dispatch since #13025 failed at Typecheck UI with
TS2307. Move the step to right after install.

* fix: run_commands object form without args routes through the shell instead of failing with ENOENT (#13336)

* fix: run_commands object form without args routes through the shell

The structured { command, args? } form of run_commands was always spawned
directly with shell: false. When a model emitted a full command line in
command with no args (e.g. { command: "echo hello" }), spawn failed with
ENOENT for any command containing a space, breaking command execution for
the whole session.

Direct exec now only applies when a non-empty args list is provided; the
object form without args is routed through getShellInvocation like the
string form. Schema descriptions are tightened so models put arguments in
args instead of embedding them in command.

Fixes #13279

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

* chore: trim structured-command schema descriptions

The union schema is only used for lenient validation of input the model
already sent; its descriptions never reach a model prompt. Keep them
short instead of restating executor behavior.

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

* chore: simplify direct-exec comment in shell executor

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

* revert: keep original structured-command schema description

The description never reaches a model prompt and the executor now handles
both shapes, so the wording change was cosmetic noise.

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

* fix: gate direct exec on args key presence, not array length

Review feedback: an explicit empty args array is intentionally structured
input and stays direct exec; only an object with no args key is treated
as a full shell command line. Matches the key-presence rule already used
by the VS Code host's formatCommandForTerminal. Also replaces the
empty-args shell test (which was PowerShell-incompatible) with a test
pinning the direct-exec contract.

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

---------

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

* fix: normalize Gemini custom base URLs for legacy host-root values (#13329)

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

* docs: add GLM-5.3 to ClinePass models and reference pricing (#13357)

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

* feat(desktop): stream run command output (#13179)

* feat(desktop): stream run command output

* fix(sdk): clean up detached command logs

* fix(sdk): reap detached logs after hub restarts

* fix(sdk): preserve live detached command logs

* fix(desktop): harden live command progress

* fix(sdk): recover detached logs for local hosts

* fix(desktop): reconcile command output tool rows

* fix(sdk): retain logs for surviving commands

* fix(core): prevent PID reuse from retaining detached logs

* fix(core): preserve detached logs on probe failures

* fix(core): retain detached logs during probe outages

* fix(desktop): resolve leftover merge conflict in messages projection test

Combine both sides of the assertion: main's incremented per-block
createdAt projection and this branch's toolCallId/hookEventName meta.

---------

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

* fix(cli): make TUI dialog colors follow theme changes live (#13355)

* fix(cli): make TUI dialog colors follow theme changes live

Dialog content previously read the static palette constant, so open
dialogs (including the theme picker itself) kept the default dark-blue
accents while scrolling through theme previews. Add getDialogPalette /
useDialogPalette, which resolve dialog colors from the active theme's
dialog accents and re-render on every theme change, and migrate all
dialog-rendered components to it.

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

* feat(cli): derive dialog panel background from the active theme

Dark themes now lift their own background one OKLAB step for the dialog
surface, so panels keep the theme's hue instead of the library's fixed
#262626. DialogThemeSync pushes the surface into the dialog container
for new dialogs and repaints open panels, so the surface also follows
live theme previews. Light themes keep the neutral dark panel to match
the dark accent fallback and the light-on-dark dialog text.

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

---------

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

* fix: skill slash commands load via the skills tool instead of expanding into the user message (#13327)

* fix(desktop): show typed slash command instead of expanded skill markdown

The sidecar expands /skill and /workflow tokens into their instructions
before dispatching, so the runtime's persisted transcript only contains the
expanded text. After a turn (and when reopening a session) the webview
re-hydrates from that history and rendered the whole SKILL.md body as the
user's message; queue events echoing the expanded prompt could also add a
second user bubble, and fresh sessions were titled with the markdown's first
line. The CLI never shows this because its TUI keeps the typed text in its
own transcript and only sends the expanded prompt to the model.

Mirror that separation inside the desktop sidecar's display boundaries:

- history projection (readSessionMessages) inverts user text that starts
  with a configured command's instructions back to '/name remainder',
  which also repairs sessions recorded before this fix
- queue snapshots and chat_queued_prompt_start events echo the typed
  prompt recorded at expansion time, so the webview's optimistic-bubble
  re-key matches again
- an untitled session sent an expanded prompt gets titled from the typed
  command instead of the instructions' first line

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

* fix(desktop): don't overwrite a mid-turn rename with the typed-command title

The untitled check ran before dispatch, so renaming a fresh slash-command
session while its first turn was running got clobbered by the post-turn
typed-command title. Re-check at write time and only replace a missing title
or the one the runtime auto-derived from the expanded prompt.

Also documents the inherent prefix-inversion ambiguity flagged in review:
text hand-typed with a command's exact instructions persists byte-identically
to that command's expansion, so stored history alone cannot distinguish them.

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

* fix(desktop): stop expanding skill commands; let the skills tool load them

Pasting the skill body into the prompt is why the transcript could ever show
it: the desktop webview re-hydrates from the runtime's persisted history, so
whatever the sidecar splices into the user message renders as if the user
typed it. The runtime already registers the skills tool, whose description
requires the model to invoke it whenever the user references a slash command
— so send the typed /skill text through and let the tool deliver the
instructions as a tool result (previously they arrived twice: pasted and via
the tool). The persisted user message, session title, and queue entries are
then simply the typed command, which deletes the typed-prompt registry, the
queue event/snapshot rewriting, and the title machinery from the previous
approach.

Workflows are not served by the skills tool and keep textual expansion, so
the read-time display inverter stays: it collapses expanded workflow prompts
— and skill prompts persisted before this change — back to the typed
/command in the history projection.

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

* feat(core): option to keep skill slash commands typed for the skills tool

resolveRuntimeSlashCommandFromWatcher (and the hub snapshot proxy) accept
expandSkillCommands: hosts whose sessions register the skills tool pass
false so the typed /skill goes through and the model loads the instructions
as a tool result, keeping the persisted transcript as what the user typed.
Workflows always expand — the tool does not serve them. isSkillsToolAvailable
exposes the catalog check hosts use to decide (yolo preset and the skills
tool toggle leave textual expansion as the only delivery path).

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

* fix(cli): skill slash commands load via the skills tool instead of expanding

The TUI user-command wrap and buildUserInputMessage now keep a typed /skill
as-is when the session's mode/toggles register the skills tool, matching the
desktop app; workflows keep expanding, and yolo (zen) keeps expanding skills
because its preset has no skills tool. This also fixes CLI resume/history
surfaces showing the skill body: the persisted user message is now the typed
command.

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

* fix(vscode): keep configured skill slash commands typed for the skills tool

expandSlashCommands no longer splices a configured skill's instructions into
the model text; the SDK session's skills tool delivers them as a tool result
(previously they arrived twice). Builtin pseudo-skills like /deep-planning
are not served by that tool and keep expanding, as do workflows.

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

* refactor(desktop): use the shared skill-expansion option in the sidecar

Replaces the sidecar's workflow-detection dance with core's
expandSkillCommands option and gates on isSkillsToolAvailable, restoring
textual expansion where the tool is missing (yolo mode or the skills tool
toggle) — a gap in the previous desktop-only change.

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

* refactor(desktop): drop the display inverter for expanded transcripts

Accepted trade-off to keep the change minimal: sessions recorded before
skills switched to the skills tool, workflow sends (deprecated), and
yolo-mode skill sends persist expanded instructions and now render that text
as-is instead of being collapsed back to the typed /command at projection
time.

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>

* Use fixed selection chevron in account dialog to match other dialogs (#13364)

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

* fix(desktop): align system prompt with session mode (#13361)

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

* fix(desktop): finalize queued turns on chat_done with canonical history reconcile (#13330)

Turns that settle through the event stream (queued prompts, including the
first prompt of a fresh session) resolve their send() RPC early, so nothing
cleared the streaming shimmer or reconciled live-streamed content against
the persisted transcript at turn end. A turn whose deltas were incomplete
stayed visually streaming forever and only healed when a later non-queued
send rehydrated history.

chat_done (and chat_session_ended / the queue-drain double check) now clears
the active assistant streaming id and schedules a short-delayed
read_session_messages + applyCanonicalHistory, guarded by turn epoch,
session id, and in-flight send submissions so it never clobbers a newer
turn or duplicates the blocking send path's own finalization.

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

* chore(desktop): release v0.0.14

* fix(clients): filter non-chat models from chat pickers (#13317)

* fix(clients): filter non-chat models from chat pickers

* fix(clients): align chat model eligibility

* fix(desktop): strip user_input envelope when copying a user message (#13369)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>

* docs: show DeepSeek V4 peak and off-peak pricing (#13312)

* docs: update DeepSeek V4 average pricing

* docs: show DeepSeek peak and off-peak pricing

* docs: add GLM-5.3 reference pricing (same as GLM-5.2)

* docs: add GLM-5.3 to ClinePass models table

* fix(llms): display billed gateway cost (#13385)

* fix(shared): run PowerShell commands with fail-fast error semantics (#13358)

* fix(shared): run PowerShell commands with fail-fast error semantics

The run_commands PowerShell wrapper never set $ErrorActionPreference, so
the default 'Continue' applied: a pipeline erroring per item (e.g. a
malformed Where-Object over Get-ChildItem -Recurse) emitted one error
record per enumerated file - tens of thousands of stderr records on
large trees, looking like a hang - and could still resolve as SUCCESS
with exit 0.

Prepend $ErrorActionPreference='Stop'; to the script content executed
by the ScriptBlock so the first error terminates the command with a
non-zero exit and a single error message. Concatenated on the same line
as the user command so error line numbers stay unshifted.

Fixes #13285

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

* fix(shared): set the fail-fast preference in the bootstrap scope

Setting $ErrorActionPreference='Stop' by string-prepending it into the
scriptblock source displaced a leading param(...) from its mandatory
first-statement position, so scripts beginning with a param block failed
with CommandNotFoundException. Preference variables are dynamically
scoped, so setting Stop in the -Command bootstrap gives the invoked
scriptblock identical fail-fast semantics while keeping the user script
byte-identical (param works, error positions unshifted) and drops the
doubled-quote escaping.

* docs(shared): document the fail-fast tradeoffs in the PowerShell wrapper

Stop promotes every non-terminating error, not only per-item pipeline
floods: partial-result commands (recursive listings over access-denied
junctions) now stop at their first error, and Windows PowerShell 5.1
turns in-script stderr redirection of succeeding native commands fatal.
State this in the wrapper comment as a deliberate tradeoff, with the
GitHub Actions precedent and the per-command opt-outs.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* ci: stop over-long changelogs from silently dropping release Slack posts (#12955)

Slack section blocks reject text longer than 3000 characters. The Slack
action logs that rejection as ##[error] but does not fail the step, so an
over-long changelog drops the release announcement while the run stays
green — cline@3.0.50 (3272 chars) published to npm, tagged, and cut a
GitHub release with no Slack post and nothing red to notice.

Every publish workflow pasted the changelog section verbatim into one
section block, so all six were exposed; the SDK, desktop, and extension
sections were only 150-350 chars under the ceiling.

Add a slack_content output alongside content: unchanged when the section
fits, otherwise trimmed on a line boundary with a link to the full
release notes. Only the Slack payload uses it — GitHub release bodies and
the desktop updater manifest still get the whole section.

* ci: tidy workflow cache config and job permissions (#13403)

Publish workflows now always do clean npm installs (no dependency
cache in their test gates), the e2e workflow's cache keys are
exact-match only, and the e2e job drops an id-token permission it
never used.

* Rename desktop app from "Cline Code" to "Cline" (#13401)

* Rename desktop app from Cline Code to Cline

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

* Format touched Rust test assertions

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

---------

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

* fix(llms): surface provider-executed tool activity as observational events (#13300)

* fix(llms): surface provider-executed tool activity as observational events

Provider-executed tool parts (e.g. every tool the Claude Code CLI runs
inside its own session) were dropped by the model-tool guard added for
web search: only declared model tools were re-emitted, everything else
hit continue with nothing yielded. Those sessions modified the workspace
with no tool activity in runtime events, transcripts, or the UI.

Route all providerExecuted parts onto the observational path instead:
emit execution-tagged tool-call-delta and tool-result events, matched by
tool-call ID for providers that omit the flag on the result half. They
stay out of AgentRuntime's execution/approval loop, and the runtime
already persists them as modelToolActivities and projects them for
display.

The AgentModelEvent tool-result variant widens toolName from
ModelToolName to string to carry the provider's own tool names.

* fix(agents): keep turns that are only provider-executed tool activity

A turn consisting solely of observational tool activity has an empty
assistant content array - the activity lives in message metadata, since
projecting it into content would replay tool_use blocks the model never
gets results for. The empty-content guard threw on such turns, erroring
the run and losing the activity from the transcript. Count model-tool
activity as content for the emptiness check (error finishes still
throw); replay stays safe through the codec's empty-content placeholder.
Also drop the trailing text delta from one gateway test so the tool-only
stream shape stays covered end to end.

* feat: allow agents to create scheduled tasks (#13331)

* feat(core, desktop): add durable todo agenda

* fix(desktop): secure todo approvals and track tool usage

* fix(desktop): clean up failed approval delivery

* fix(desktop): authenticate approval connections

* fix(desktop): cancel approvals on broadcast failure

* fix(desktop): authenticate development approvals

* fix(desktop): harden development approvals

* test(core): make task paths cross-platform

* fix(desktop): serialize approval readiness

* refactor(core): unify todo and schedule tools

* feat(core): distinguish user todos from agent suggestions

* fix(core): hide tasks tool in yolo mode

* fix(core): enforce schedule workspace scope

* fix(core): bind schedule scope to hub connection

* fix(core): establish task scope at hub startup

* fix(core): scope task automation by workspace

* test(core): normalize workspace path expectations

* test(core): serialize Windows CI workers

* fix(core): reject unregistered schedule authority

* fix(desktop): guard task execution commands

* fix(core): avoid polynomial regex in mention parsing

* fix(core): address schedule tool review feedback

* fix(core): bind websocket clients to hub workspace

* fix(core): flatten tasks tool input schema

* fix(core): authorize multi-workspace hub clients

* test(core): type hub transport authority mock

* fix(cli): register a workspace client for remote schedule commands (#13398)

---------

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

* fix(desktop): treat ClinePass as OAuth-managed in the chat credential gate (#13404)

* fix(desktop): treat ClinePass as OAuth-managed in chat credential gate

ClinePass shares the Cline account OAuth credentials (its auth handler
stores under the "cline" provider), so the webview never sees a plain
API key for it. The chat pre-flight check only exempted cline/oca/
openai-codex, so switching to ClinePass while signed in via OAuth
blocked with "Missing API key" even though the sidecar resolves the
stored access token fine (which is why the CLI worked).

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

* style: format helpers.test.ts with biome

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

---------

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

* fix(ui): stack code block lines when streamdown lineNumbers is off (#13412)

streamdown renders each Shiki token line as a bare inline span with no
newline text between non-empty lines, and only applies its block line
class when lineNumbers is on. With lineNumbers off (the desktop app's
config) every multi-line fenced block collapsed into one run-on line.
Make the direct line spans under code-block-body display: block in the
shared markdown.css; empty lines keep their height via their lone "\n"
child under white-space: pre.

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

* fix(desktop): work summary undercounts wall time when pre-tool thinking attaches to the answer (#13413)

* fix(desktop): anchor work summary duration on the answer row, not attached pre-tool reasoning

The collapsed 'Worked for Xs' row undercounted wall time whenever a turn's
assistant message contained thinking + tool_use with no narration text: the
canonical projection emitted the reasoning-only row after the tool row (both
stamped before the tool executed), the webview attached that row to the final
answer, and collapseCompletedWork used the answer's earliest attached
reasoning timestamp as the end anchor - excluding the entire tool execution
(e.g. 'Worked for 5s' for a turn with an 8s command).

- webview: end the work span at the answer row's own timestamp, clamped to
  the last collapsed row so a fallback answer bubble with a synthetic early
  timestamp cannot shrink the duration either
- sidecar: flush pending thinking before a tool_use row so rehydrated
  transcripts keep the live-stream order (thinking before its tool call) and
  pre-tool reasoning no longer rides on the next answer

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

* fix(desktop): keep interleaved thinking between the tool calls it separates

Address Greptile review: when one assistant message interleaves thinking
between multiple tool_use blocks, each reasoning segment now projects at its
own position (attached to a text row from its own segment when present,
otherwise as its own row) instead of merging into the first reasoning row,
which displayed later thinking before a tool call it actually followed.

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

---------

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

* fix(desktop): remove settings gear hover state while Account screen is open (#13408)

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

* fix(desktop): don't show "No sessions found" while session history is still loading (#13414)

* fix(desktop): don't show 'No sessions found' while session history is still loading

Replace the isLoadingHistory flag with hasLoadedHistory, set only once the
backend has actually answered a list_discovered_sessions request. The sidebar
and Sessions view now keep their loading state until that first definitive
response, so the empty-state copy can no longer appear while history is still
being fetched (or while a failed fetch is being retried).

Also retry a failed initial fetch on the 2s event cadence instead of stranding
the UI until the 12s periodic poll, which is what stretched the misleading
empty state to ~10 seconds after a webview reload when the websocket lost the
race with the page load.

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

* fix(desktop): stop history fast-retry from re-arming after hook unmount

A failed initial fetch that settles after the hook unmounted could schedule a
new retry timer after cleanup had already cleared the refs, leaving the
abandoned hook polling the backend every 2s. Guard scheduleRefresh with a
disposed ref set by the mount effect's cleanup.

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

---------

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

* Fix @ file mentions breaking on paths with spaces (#13391)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

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

* Fix import ordering in mentions test (biome organize imports)

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

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

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

* Normalize mention paths to posix separators for Windows

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

---------

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

* Fix code actions failing with "command not found" on VS Code 1.134 (#13402)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

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

* Fix import ordering in mentions test (biome organize imports)

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

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

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

* Normalize mention paths to posix separators for Windows

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

* Fix code actions failing with 'command not found' on VS Code 1.134

Code action commands carried arguments (expandedRange, diagnostics),
which routes them through VS Code's CommandsConverter cache. VS Code
1.134 disposes the cached entries before the clicked action executes,
so every lightbulb action failed with 'Actual command not found,
wanted to execute cline.addToChat'.

Drop the arguments so the command id is passed through directly, and
recover the context in the handler instead: getContextForCommand now
expands an empty selection by 3 surrounding lines (matching the old
provider behavior) and gathers document diagnostics intersecting the
range when none are passed explicitly.

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

* Scope gathered diagnostics to the selection/cursor

Match the old CodeActionContext.diagnostics behavior: only include
diagnostics intersecting the range the action was requested for, not
the surrounding lines the text gets expanded to.

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

---------

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

* Desktop: unify Plugins, MCP, and Skills into one Plugins hub with a dedicated Marketplace page (#13411)

* Unify desktop plugins, apps, MCP, and skills into one Plugins hub with a Browse directory mode

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

* Open the marketplace directory as a modal over the Plugins hub instead of swapping the page

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

* Rename directory to Marketplace: Browse Marketplace button, Marketplace modal title with icon, search placeholder

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

* Fix search input focus ring clipped by the Marketplace modal scroll container

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

* Address Greptile review: keep selected tag chip visible when its count drops to zero, and remount installed tab when a marketplace install completes after the modal closed

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

* Track marketplace modal mutation flag in a ref so a close click racing a queued render cannot skip the inventory remount

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

* Make Marketplace its own settings page under Customizations and restore Channels as a standalone page

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

* Remove icon from Marketplace page header for consistency with other settings pages

* Notify mounted inventory views when the marketplace invalidates the cache so late install completions refresh the Plugins hub

---------

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

* feat(desktop/ui): recommended and free model tiers in the composer model selector (#13410)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

---------

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

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK (#13415)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

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

* feat(desktop): recommended-feed badges and descriptions in provider settings (#13416)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* feat(desktop): recommended-feed badges and descriptions in provider settings

Review suggestion on #13410: the provider settings page has room for
more model detail than the composer's picker. The cline/cline-pass
provider cards now refresh their model list through
list_provider_models (the catalog snapshot deliberately skips the
recommended-feed overlay so the startup catalog fetch never blocks on
the feed) and render Recommended/Free tier badges plus feed tags (NEW)
next to the model name, with the model description underneath. The
refreshed list also surfaces the live entries instead of the bundled
snapshot.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

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

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

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

* fix(desktop): scope the settings featured model list to its provider and revision

The fetched featured list was unscoped component state: switching
between cline and cline-pass reused the component instance, so the
previous provider's models stayed visible while the new request was
pending (or forever, when it failed), and the retained copy shadowed
later provider.modelList updates — adding a second custom model
submitted the stale list as the complete configuration and dropped the
first addition.

The fetched list now only applies to the provider and modelList
revision it was fetched for (falling back to the catalog snapshot
otherwise and refetching on membership changes), and add-model submits
the union of the displayed and configured ids so an update can never
silently unconfigure existing entries.

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

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

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

* fix(ui): update packed-Tailwind smoke contract for the picker's max-h-64 (#13421)

The ui-publish smoke check pins a set of Tailwind candidates the packed
sources must emit; #13410 grew the SearchCombobox options list from
max-h-56 to max-h-64, so the publish run failed on the stale candidate.
All other pinned candidates verified against the current sources.

* feat(desktop): refresh app icons and branding (#13400)

* ci(vscode): upload E2E failure recordings from the right path (#13427)

The job sets working-directory: apps/vscode, but that default applies to run
steps only, not to `uses:` steps. Since #10961 moved the extension under apps/
and added that default, the artifact path has resolved against the repo root,
matched nothing, and every failing run logged "No files were found with the
provided path: test-results/playwright/" instead of uploading recordings.

Widen to test-results/ so Playwright's error-context snapshots ship alongside
the videos.

* fix(hooks): deliver tool hook contextModification to the model (#13297)

* fix(hooks): deliver tool hook contextModification to the model

On the next engine, a tool_call (PreToolUse) hook's contextModification
was parsed into HookControl.context and then silently dropped: the
runtime beforeTool/afterTool result contract had no channel for
injecting conversation context. Legacy consumed it (ToolExecutor /
ToolHookUtils pushed <hook_context> blocks into the next user turn), so
this was a regression of documented behavior.

- Add appendContext to AgentBeforeToolResult/AgentAfterToolResult.
- AgentRuntime collects appendContext across hooks during an
  iteration's tool executions and appends one <hook_context> user
  message after the tool results, keeping tool-result parts contiguous.
- Map HookControl.context into appendContext in both subprocess hook
  layers (skipped when the hook cancels, matching legacy, where the
  message doubled as the error).
- Truncate injected context at 50KB per hook output, matching legacy.
- Concatenate appendContext across merged hook layers.

tool_result (PostToolUse) hooks still run detached with stdout ignored;
making them blocking so their context can be collected is a follow-up.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): stamp tool identity on injected hook context blocks

Contexts are batched into one message after the tool results, and
parallel tool execution collects them in completion order, so position
alone cannot attribute a block to its tool call. Add tool_name and
tool_call_id attributes to each <hook_context> block.

* fix(hooks): sanitize hook context block markup

Attribute values (tool_name, tool_call_id) are stripped of quote/angle
characters and embedded </hook_context> closers in hook output are
neutralized, so neither provider-supplied ids nor hook text can corrupt
or spoof a block's stamped identity.

* fix(hooks): neutralize forged opening hook_context tags in hook output

The previous sanitization only neutralized closing tags, so hook output
could still open a forged <hook_context> block claiming another tool's
identity. Escape both opening and closing embedded tags with one rule.

* fix(hooks): hide injected hook context from user-facing transcripts

Stamp the injected hook-context user message with displayRole 'system'
(the compaction-summary convention) so it reaches the model but does
not render as a user bubble in live or replayed transcripts. Without
this, resuming a session showed the raw <hook_context> block as if the
user had typed it.

* fix(hooks): neutralize case-variant embedded hook_context tags

The tag-neutralization regex was case-sensitive, so hook output could
still smuggle a forged tag as <HOOK_CONTEXT>. Match case-insensitively.

* fix(vscode): map PreToolUse contextModification into runtime appendContext

The extension's hooks adapter bridged file hooks into the SDK runtime
but forwarded only cancel/errorMessage, so a PreToolUse hook's
contextModification never reached the model. Map it into the runtime's
appendContext channel; HookFactory already truncates it at 50KB.

* fix(vscode): hide hook-injected context from replayed transcripts

Live sessions never rendered the injected <hook_context> user message,
but session reload replayed it as a user bubble (and post-resume turns
kept doing so). Treat these messages as synthetic in the user-message
mapping: honor the displayRole 'system' stamp the runtime sets, with a
text-prefix guard for paths where metadata is unavailable. This also
keeps edit/regenerate ordinal mapping aligned with visible bubbles.

* fix(hooks): run file hooks through exactly one layer per host

The VS Code extension registered two independent hook execution layers:
its own hooks adapter (config.hooks) and the SDK core's file-hook
extension from the runtime bootstrap. When both discover the same hook
files, every hook executes twice per event — and with context injection
wired, each contextModification would be injected twice.

Add a 'hooks' runtime config extension kind (in the default set, so the
CLI keeps core file hooks unchanged) and gate the bootstrap's file-hook
extension on it. The extension excludes 'hooks' at session start, so
its adapter — which also provides the hook status UI and the
hooksEnabled setting — is its single execution path.

* fix(vscode): discover hooks from the session workspace, not only global state

Hook discovery read workspaceRoots from global state shared across
every Cline instance, so another window repointing it made workspace
hooks silently stop being discovered. With the extension's adapter now
the single hook execution layer, that meant no hooks at all.

HookFactory takes an optional sessionWorkspaceRoot and unions that
root's .clinerules/hooks into discovery (and into cwd resolution), fed
from the session config's cwd. Shared-state discovery still works, so
behavior in the single-window case is unchanged.

* fix(hooks): keep sanitized hook attribute values distinguishable

Replacing every markup delimiter with the same underscore could
collapse two tool call ids that differ only by such a character into
identical stamps. Escape each delimiter with a distinct token instead.

* fix(hooks): make hook attribute sanitization injective

Escaping the underscore itself turns the attribute escaping into a
uniquely decodable code, so no two distinct tool call ids can collapse
to the same sanitized stamp (previously an id containing a literal
escape token could collide with an id containing the delimiter).

* fix(vscode): reconstruct hook status rows when replaying transcripts

hook_status messages are emitted live but never persisted, so reloading
a session dropped every hook row. The injected <hook_context> blocks
carry the hook source and tool name, so the replay translator now
rebuilds a completed hook status row from each block. The injection is
also no longer treated as a user turn boundary, so the final turn's
completion retag is unaffected by it.

* fix(hooks): collect PostToolUse hook output and honor its control (#13298)

* fix(hooks): collect PostToolUse hook output and honor its control

tool_result (PostToolUse) hooks ran fire-and-forget with stdout
ignored, so their entire JSON output — contextModification and cancel —
was discarded. Legacy awaited PostToolUse, injected its
contextModification into the conversation, and honored cancel.

- Run tool_result hook commands blocking (same 120s default timeout as
  tool_call) in both the hook-config-file layer and the agent-hook
  subprocess layer.
- Map their output: cancel stops the run with the hook's error message
  as the reason; otherwise context is injected via afterTool
  appendContext.

This restores legacy blocking semantics: tool results now wait for
tool_result hooks, but only in sessions that have one configured.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): bound tool_result hook wait and isolate cancel reason

Address review findings:
- The agent-hook subprocess layer forwarded an unset timeoutMs
  unchanged, so a tool hook command that never exits would block the
  agent indefinitely. Default both tool_call and tool_result to the
  120s bound the hook-config-file layer already used.
- A cancelling hook's error message was folded into the same context
  field as other hooks' injectable context, so merging controls could
  leak unrelated hook context into the cancellation reason. Carry it as
  a separate cancelReason, and surface it as the stop reason for
  beforeTool cancels too.

* fix(hooks): prefer errorMessage as a cancelling hook's stop reason

When a cancelling hook returns both contextModification and
errorMessage, the context-first parse precedence made the injectable
context the cancel reason and discarded the actual error. Parse the two
fields separately: errorMessage wins as the cancel reason (matching
legacy), and a lone errorMessage still folds into injectable context
for non-cancelling hooks as before.

* fix(vscode): honor PostToolUse hook cancel and contextModification

The adapter awaited PostToolUse hooks but discarded their output
entirely. Map cancel to a stop control (with errorMessage as the
reason) and contextModification into the runtime appendContext channel,
matching the PreToolUse mapping and legacy semantics.

* fix(hooks): whitespace-only errorMessage no longer suppresses the cancel reason

A cancelling hook returning meaningful context alongside a blank
errorMessage lost both: the parsers selected the whitespace as the
reason and the result mappers trimmed it away. Require a non-blank
errorMessage before it wins, so context serves as the fallback reason.
Apply the same fallback in the extension adapter's stop mapping.

* fix(core): stop Windows CI worker crashes from the agenda spec watcher (#13428)

* fix(core): watch agenda task specs via the resolved long path

fs.watch on a path with 8.3 short components (e.g. C:\Users\RUNNER~1
temp dirs) trips a libuv assertion in fs-event.c on Windows and aborts
the whole process. Since the agenda task manager landed, every hub
server test spins up its spec watcher on such a path on hosted Windows
runners, killing the vitest worker and failing the sdk-test Windows job
on every branch. Resolve the specs dir with realpathSync.native before
watching so libuv only ever sees the long form.

* test(ui): stub ResizeObserver for @pierre/diffs in tool-diff tests

jsdom does not implement ResizeObserver, so every ToolFileDiff render
logged a ReferenceError from @pierre/diffs to stderr. Tests still
passed; this just silences the noise the same way the constructable
stylesheet shim does.

* fix(core): skip the agenda spec watcher when the dir does not resolve

Falling back to the raw path on realpath failure would reintroduce the
Windows short-path abort; log and go without the watcher instead.

* fix(vscode): honor the classic truncation range when migrating legacy tasks (#13419)

Classic Cline truncated long conversations by omitting an index range of
api_conversation_history from every API request (keep the first
user-assistant pair, drop everything through the range end, strip
orphaned tool_results from the first kept message). The range was
persisted on the history item while the full history stayed on disk.

legacyApiHistoryToSdkMessages ignored conversationHistoryDeletedRange
and converted the entire file, so resuming a migrated long task handed
the SDK an untruncated working context that could exceed the model's
context window by millions of tokens - every request failed with
'prompt is too long' and every compaction restarted from the full
history (#12996, confirmed by the reporter: the task was migrated from
an older version and broke after a restart, with each compaction
starting from ~3M tokens).

The migration now replays exactly what the classic extension sent:
slice out the deleted range and drop orphaned tool_results, mirroring
ContextManager.getTruncatedMessages (see origin/main). Malformed ranges
fall back to the full history (previous behavior).

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

* fix(vscode): show the diff edit view for multi-line edits in CRLF files (#13417)

The edit preview computed proposed content with an exact old_text match, but
the SDK executor normalizes old/new text to the file's own line endings before
matching (#12305) - reads strip CR, so models emit LF-only text even for CRLF
files. Any multi-line old_text in a CRLF file therefore failed the preview's
match: the diff edit view silently never opened while the executor applied the
edit. Single-line edits (no line break in old_text) were unaffected, which is
why the diff view appeared to trigger inconsistently.

Mirror the executor's EOL normalization (and its literal $-sequence insertion)
in the preview computation.

Fixes #13296

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

* fix(core): report truthful session status so desktop checkpoint restore stops wedging (#13418)

* fix(core): keep hub session status truthful across queue-drained turns

Queue-drained turns settle only through the event stream, but the hub
runtime host mistranslated their lifecycle in two ways:

- session.updated events carrying only a snapshot (persistence updates)
  defaulted the projected status to "running". When one trailed the
  final idle update after a turn, clients that track busy state from
  status events (the desktop sidecar's workspace restore gate) stayed
  busy forever. Use the snapshot's real status and emit nothing when
  neither source reports one.
- the per-run agent.done dedup was only reset by run.started, which the
  daemon-side queue drain never publishes, so a drained turn's done was
  swallowed as a duplicate of the previous turn's. Reset the dedup on
  session.pending_prompt_submitted, and suppress stale run.completed
  events that land inside a drained turn's window so they can neither
  emit a phantom done nor consume the drained turn's dedup slot.

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

* test(desktop): cover restore unlock after an event-settled queued turn

Exports the sidecar's core-session event handler so the queued-turn
lifecycle (busy via status events, cleared by the done agent event,
restore allowed afterwards) is testable end-to-end.

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

* fix(core): start interactive sessions without a prompt as idle

The runtime host reported every new session as "running" until its
first turn ended. Interactive hosts (the desktop app) start sessions
with no prompt and dispatch turns through separate send calls, so a
created-but-never-prompted session stayed "running" forever — wedging
clients that gate workspace operations (checkpoint restore, message
edit) on active turns.

Interactive no-prompt starts now begin idle, start emits the session's
actual status (resumed sessions no longer masquerade as running), and
markTurn* transitions keep tracking in-memory status for lazily
persisted sessions so the first turn still reports running -> idle.

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

* style: format hub-runtime-host test filter

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

* refactor: drop the drained-turn done bookkeeping, keep the minimal fix

The stuck restore is fully explained by the two status defects (fabricated
"running" from snapshot-only session.updated events, and never-prompted
interactive sessions reporting "running"). The done-dedup machinery for
queue-drained turns addressed a separate cosmetic gap (queued turns emit no
chat_done, pre-existing) and required fragile run-window heuristics, so it
is removed to keep this change reviewable. Sidecar test now settles the
queued turn through the status event, matching the shipped mechanism.

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

* docs(sdk): document the truthful session-status contract

---------

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

* chore(deps): update Langfuse packages and bump app versions (#13443)

* chore(deps): update Langfuse packages and bump app versions

Update @langfuse/otel to v5.10.1 and add @langfuse/vercel-ai-sdk v5.9.1 for improved observability with Vercel AI SDK.

Bump versions for @cline/code to 0.0.14 and @cline/ui to 0.2.0-next.6, updated via bun.lock.

Other Changes:
Added optional userId to AgentRuntimeConfig.
Propagated userId, sessionId, conversationId, runId, iteration, provider, and model context into AI SDK telemetry.
Added AI SDK 7 runtimeContext with explicit includeRuntimeContext.
Added stable OTEL_SERVICE_NAME=cline-sdk.
Added runtime metadata assertions in agent tests.

* add taskId

* Revert "add taskId"

This reverts commit f20d31d96d.

* docs: simplify Open Cline step in installing guide (#13405)

* docs: remove duplicate GLM-5.3 rows in ClinePass tables (#13449)

Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>

* chore(sdk): release v0.0.76

* chore(cli): release v3.0.56

* docs(cli): scope the v3.0.56 release notes to CLI-visible changes

* feat(desktop): interactive welcome hero graphic (#13399)

* feat(desktop): add interactive welcome hero

* feat(desktop): support composable welcome hero variants

* feat(desktop): reskin first-run onboarding (#13441)

* refactor: centralize client tool availability (#13451)

* chore(sdk): release v0.0.77

* docs(cli): drop the tasks tool from the v3.0.56 notes, it is desktop-only

* chore(vscode): prepare 4.1.11 release

* chore(desktop): release v0.0.15

* fix(vscode): remote config MCP settings (#13466)

* fix(vscode): enforce enterprise MCP controls on the Customize marketplace

The unified Customize marketplace replaced the old MCP marketplace
without carrying over enterprise remote-config enforcement: the catalog
RPC returned every MCP entry and installs were never policy-checked,
so orgs with mcpMarketplaceEnabled=false or an allowedMCPServers
allowlist saw (and could install) all marketplace MCP servers.

- Filter MCP entries out of getMarketplaceCatalog when the marketplace
  is disabled, and restrict entries to the allowlist when configured
  (matching entry id, display name, installed server name, or source
  repo URL, mirroring legacy GitHub-URL allowlist ids)
- Reject installMarketplaceEntry requests that violate the policy
- Map the published catalog's repo/homepage fields onto
  sourceUrl/homepageUrl so URL-based allowlists can match
- Update the enterprise MCP server controls docs

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

* refactor: simplify MCP marketplace policy enforcement

Fold the policy check into marketplace-helpers, drop the dedicated
test suite, and trim the docs edit to the strictly necessary line.

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

---------

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

* Treat an empty preserved capability list as unspecified when seeding tools (#13465)

* Treat an empty preserved capability list as unspecified when seeding tools

toSdkModelInfo guarded the tools seeding with a strict
preservedCapabilities === undefined check, but modelHasCapability —
the runtime's own reader — treats undefined AND length === 0 as
"unspecified". A custom OpenAI-Compatible model whose stored
capabilities field is a defined-but-empty array (a config carried over
from before the field existed, or one round-tripped through a boundary
that defaults it to []) skipped the seeding; the first boolean
projection to run afterwards (e.g. supportsReasoning) then populated
the array, the runtime gate read the non-empty, tool-less list as
authoritative, and every tool definition was silently dropped from the
session (#13463).

The guard now covers the empty array too, matching the reader's
unspecified semantics.

* test: satisfy the store's isModelInfo gate so the empty-capabilities case actually reaches knownModels

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

---------

Co-authored-by: yzxcj797 <yzxcj797@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>

* chore(vscode): prepare 4.1.12 release

* Add feature flags to the desktop app (#13289)

* Add feature flags to the app

* React to account updates

* Address comments

* use a per-app file

* fix: propagate Langfuse session telemetry (#13473)

* fix telemetry session propagation

* feat telemetry client version metadata

* fix(core): address Langfuse review feedback — hub client identity + delegated agent session grouping (#13475)

* fix(core): rebuild hub session client identity from request headers

Hub-backed sessions do not transport extensionContext (it is local-only),
so the daemon's runtime built traces without the clientName/clientVersion
metadata even though the hub client bakes X-CLIENT-TYPE / X-CLIENT-VERSION
into the session's provider headers. Reconstruct extensionContext.client
from those headers during local runtime bootstrap so hub-backed Langfuse
traces carry the same client identity as local runtimes, and the daemon's
header re-resolution stops clobbering the original X-CLIENT-TYPE.

* fix(core): propagate parent distinctId/sessionId to delegated agents

Delegated agents (spawned sub-agents, configured agents, teammates) were
built without distinctId and sessionId, so their Langfuse traces had no
userId or sessionId and did not group with the parent user or session.
Thread the host-resolved distinctId through RuntimeBuilderInput and the
root sessionId through the delegated-agent config provider, and copy both
onto the delegated AgentConfig.

---------

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

---------

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

* ci(vscode): make combined nightly manual-dispatch only

The PublishNightly environment gained required reviewers, so each cron
run parked on approval, held the workflow's concurrency group, and
silently cancelled every scheduled run queued behind it. 20 consecutive
scheduled nightlies died this way between 2026-07-31 and 2026-08-21;
the only nightlies that shipped in that window were manual dispatches.

Drop the cron rather than leave a trigger that cannot succeed unattended.

* feat(hub): add drain and upgrade commands with replay support (#13468)

* feat(hub): add drain and upgrade commands with replay support

* handles disconnection

* feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport

Completes the wiring the previous commits' primitives needed:
HubServerTransport gains isDraining(), hub.drain/hub.status/profile.get
command handling, and replayEventsAfter() (backed by the durable event
log), plus the sequence/sinceSequence wire types they depend on in
shared/hub.ts. run-queue-handlers.ts reads the active bot profile's
plugin roots when executing durable runs.

Also adds hub/profiles/: profile.json (identity/rules/plugins) ->
system prompt composition, --profile / CLINE_HUB_BOT_PROFILE
resolution, and the bundled cline-dad profile with its
cline_hub_support read-only diagnostics tool.

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

* Revert "feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport"

This reverts commit 6696d5d202.

* fix(hub): dedupe replayed events by eventId, not just sequence

HubEventLogStore.append() returns a new envelope stamped with a
sequence rather than mutating the input, so a pending approval
re-issued sequence-less by subscribe() (it predates any durable-log
append) and its later sequence-stamped copy from the durable log are
two different objects carrying the same eventId. The replay-then-live
buffer in browser-websocket.ts only deduped by sequence, so the
sequence-less copy's guard never tripped and it was delivered a second
time when the buffer flushed after replay.

Track delivered eventIds alongside the sequence cursor; eventId
survives the append/stamp round-trip unchanged, so this dedupes the
exact-same logical event regardless of which copy arrives first.

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

* fix(hub): wire drain, durable event log, and run queue into the live transport

CI on this branch failed bun run build:sdk: browser-websocket.ts,
client/index.ts, and hub-websocket-server.ts (already on this branch)
reference sequence/sinceSequence, HubServerTransport.isDraining(), and
the "hub.drain" command — but the commit that reverted bot profiles
out of this branch also reverted this wiring, since it shared a commit
with the profiles work. That wiring is a hub concern, not a
bot-profiles one; split it back out.

- shared/hub.ts: sequence/sinceSequence types, run.enqueue/run.list/
  hub.drain/hub.status/stream.replay capability, command, and event
  names. profile.get intentionally excluded — stays bot-profiles-only.
- context.ts: isDraining() on HubTransportContext. botProfile field
  intentionally excluded.
- hub-server-transport.ts: eventLog/runQueue fields and start/stop
  lifecycle, publish() appends to the durable log, handleCommand cases
  for run.enqueue/run.list/hub.drain/hub.status, drain-refusal check,
  replayEventsAfter()/lastEventSequence(). startBotProfile()/
  startHubSupportTool() and the profile.get case intentionally
  excluded.
- run-queue-handlers.ts: added without handleProfileGet (needs
  ctx.botProfile, which doesn't exist here).
- hub-upgrades.test.ts: added without its two bot-profile-injection
  tests (they need a resolved bot profile to assert against).

Verified bun run build:sdk exits 0 (the exact CI command) and
bunx vitest run src/hub passes (311/312; the one failure is the
same pre-existing environment-timing flake already present before
this change).

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

* fix(hub): export instance-lock, event-log, and run-queue from the hub barrel

These landed as internal modules only; hub-server-transport.ts and
hub-websocket-server.ts import them by direct path, but nothing
re-exported them from the public @cline/core/hub surface the way
sibling discovery/server modules already are.

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

* fix(hub): wire the instance lock into the daemon entry point

The singleton lock (discovery/instance-lock.ts) and its consumption in
startHubWebSocketServer/ensureHubWebSocketServer were already on this
branch, but the daemon entry point's own half was not: retrying a bind
when a retiring predecessor still holds the lock, and exiting with a
distinct code (3) instead of the generic fatal path when a live Hub
already owns the data directory. Without this, a daemon racing a
retiring predecessor could fail outright instead of waiting the lock
out, and losing the singleton race looked identical to a crash.

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

* fix(hub): address drain/upgrade review findings (#13478)

- cline hub upgrade: check idleness at least once (--wait 0 works), reject
  non-numeric --wait, and un-drain on every abort path so an aborted
  upgrade can never leave the hub refusing new work
- add cline hub drain --off and the off query param to requestHubDrain so
  POST /drain?off is reachable from shipped code
- HubEventLogStore/HubRunQueue: WAL journal mode + busy_timeout, and stamp
  sequences from lastInsertRowid instead of SELECT MAX(sequence)
- HubInstanceLock.acquire: degrade to an unheld lock when SQLite is
  unavailable instead of refusing hub startup; only BUSY/LOCKED still
  raises HubLockHeldError
- ensureHubWebSocketServer: retire an unusable discovered hub through the
  shared retireDiscoveredHub (busy hubs are attached to, drain precedes
  shutdown, discovery cleared only when the hub actually retired)
- replay adapter: advance the cursor past eventId-deduped events, cap
  replay pages, stop when the cursor stalls, and drop the dedupe set after
  the buffered flush so it cannot grow for the socket lifetime

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

* fix(hub): derive the singleton e2e challenger cwd portably

The challenger's working directory was derived by round-tripping the
discovery path through a file: URL and stripping the last pathname
segment. On Windows that yields a POSIX-style '/C:/...' path, which is
not a valid spawn cwd, so the spawn fails ENOENT before the singleton
lock is ever contested and the Windows SDK test job goes red.

The data dir is simply the discovery file's parent: use dirname().

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop stored capability lists from silently revoking tool calling for custom models (#13476)

* fix(core): seed tools capability when custom model capabilities are synthesized from boolean flags

For a models.json entry with no explicit capabilities list, toStoredModelInfo
synthesized a capability array purely from boolean convenience flags (e.g.
supportsReasoning: true -> ["reasoning"]). modelSupportsToolCalling fails open
only for a missing or empty list, so the synthesized non-empty list read as an
authoritative denial and silently stripped every tool definition from requests
to custom OpenAI-compatible models (#13463).

Seed "tools" whenever the list was not explicitly authored and the boolean
projections made it non-empty, preserving the fail-open contract. Explicitly
authored capability lists remain authoritative and can still disable tools.

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

* test(core): cover stale catalog capability overrides

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

* fix: treat stored capability lists as non-authoritative for tool calling

The hasExplicitCapabilities guard still let two producers of tool-less
lists through:

- The VS Code legacy-override migration (legacyModelInfoToOverrides)
  persists explicit partial lists like ["prompt-cache"] into models.json
  for custom OpenAI-compatible models, which then read as an authoritative
  "cannot call tools" and drop every tool - same symptom as #13463.
- Any hand- or UI-authored partial list on a non-catalog model.

Stored entries and user-authored provider metadata have no way to declare
"cannot call tools" (there is no supportsTools field, and every writer
that authors a full list includes "tools"), so seed "tools" into any
non-empty list for a language model. Only generated catalog capabilities
remain authoritative - a genuine no-tools catalog model stays that way -
and non-language models (e.g. image generation) never gain a tools claim.

Also make legacyModelInfoToOverrides write "tools" into the arrays it
fabricates, matching the providers.json migration, so models.json stops
being poisoned for older readers.

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

---------

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

* chore(vscode): prepare 4.1.13 release

* chore(sdk): release v0.0.78

* chore(cli): release v3.0.57

* fix(core): run hub e2e files serially so daemon timing budgets survive CI contention

singleton.e2e.test.ts (added in #13468) spawns real daemons and runs for
~15s. Vitest's default file parallelism let it run alongside
shutdown.e2e.test.ts, whose assertions are wall-clock bound: discovery
within 10s, exit within 5s, and a 2s shutdown watchdog. On the 2-core
windows-latest runner that contention alone broke those budgets, failing
the shutdown test two different ways across runs — once never observing
discovery, once with the daemon forced to exit before its HTTP 202
flushed (socket hang up). The test passed on Windows before #13468 and
has failed every SDK publish run since.

* chore(desktop): release v0.0.16

* test(sdk): give windows-sensitive suites realistic timeouts

Four consecutive SDK publish runs failed on windows-latest, each on a
different test, all of them plain timeouts: two @cline/shared SQLite
tests at the 5s vitest default, core's bash executor at 10s, and the hub
singleton endpoint test at 10s. The 2-core Windows runner spawns forks
and takes SQLite locks slowly enough to blow those budgets under load.

These timeouts guard against hangs; they are not timing assertions (the
one suite that does assert elapsed time, shutdown.e2e, was fixed by
removing file-level parallelism instead). Raise core to 20s and give
@cline/shared an explicit 15s in place of the inherited 5s default.

* fix(telemetry): emit task.completed from every session teardown path (#13489)

The task.completed fallback lived only inside shutdownSession, but
stopSession/dispose route interactive sessions with a terminal reported
status through releaseSessionRuntime, which never emitted. Truthful
session-status reporting (shipped in 4.1.11) re-routed a large share of
interactive stops onto that branch and silently dropped the event.

Route the emission through a single choke point,
emitTaskCompletedOnTeardown, called from both shutdownSession and
releaseSessionRuntime. The completion criterion no longer reads
session.status: interactive sessions use the recorded final-turn
outcome (lastInteractiveTurnFinishReason), non-interactive sessions
keep the existing input.status === "completed" logic. A new
taskCompletedEmitted flag (also set by the submit_and_exit observer)
enforces exactly one task.completed per session. failSession now
records the errored final turn so a stale "completed" from an earlier
turn can never leak into the teardown emission. Telemetry only; no
user-facing behavior changes.

* chore(vscode): release v4.1.14

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on (#13498)

* fix(vscode): honor MCP auto-approve settings for SDK tool calls

The SDK extension required both the global 'Use MCP servers' auto-approve
toggle AND each tool's per-tool autoApprove flag before silently approving
an MCP call, while the legacy extension treated them as either/or. Restore
the legacy OR semantics so toggling MCP auto-approve works again.

Also key toolPolicies by the registered SDK tool name (via
defaultMcpToolNameTransform, now exported from @cline/core) instead of raw
server__tool. Servers whose names contain sanitized characters (e.g.
marketplace names like github.com/user/repo) or exceed 64 chars produced
policy keys that never matched the registered tool, so those MCP tools ran
without any approval gate; the live auto-approve lookup now re-applies the
transform instead of string-splitting the name.

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

* Revert "fix(vscode): honor MCP auto-approve settings for SDK tool calls"

This reverts commit 86c568fbba.

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on

The SDK extension only auto-approved an MCP call when the global 'Use MCP
servers' auto-approve toggle AND that tool's per-tool autoApprove flag were
both set, so toggling MCP auto-approve appeared to do nothing and users had
to opt in each tool individually. The toggle alone now governs all MCP
tools; the per-tool flag is no longer consulted.

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

---------

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

* chore(vscode): release v4.1.15

* fix(cli): remove the $4.99 ClinePass promo copy (#13514)

The $4.99 first-month promo is ending, so the CLI's first-launch "Try ClinePass" dialog should no longer advertise it. Also drops the leftover CLI_PROMO_CODE plumbing, which has been an empty string since the promo-code flow was removed.

* fix(vscode): resolve hook workspace identity from the window, not shared global state (#13352)

* fix(vscode): resolve hook workspace identity from the window, not shared global state

Hook discovery, hook cwd selection, and the workspaceRoots metadata passed
to hook scripts all read the workspaceRoots/primaryRootIndex global state
keys. Global state lives in ~/.cline and is shared by every Cline instance
(all VS Code windows, the CLI, the JetBrains plugin), and nothing writes
these keys anymore, so hooks resolved against whatever project some other
or older instance last recorded. With a second window open on another
project, a workspace's .clinerules/hooks scripts were never discovered.

Resolve workspace roots via a single guarded helper backed by
HostProvider.workspace.getWorkspacePaths() (in-process, window-scoped,
same as refreshHooks): blank paths are filtered, a host-bridge failure
degrades to no workspace roots instead of silently disabling global hooks
or skipping blocking PreToolUse guards, and one resolution is threaded
through hooks-dir discovery, cache misses, cwd selection, and hook input
metadata so they can't disagree (previously up to four host lookups per
hook execution — real gRPC round trips in the standalone host). Roots and
hooks dirs are matched on whole path segments with the longest root
winning, so prefix-sharing or nested workspace roots resolve to the right
project. The adapter creates the runner once per event and skips no-op
runners, making creation the single resolution point; the separate
hasHook/getHookInfo checks are removed. The dead workspaceRoots and
primaryRootIndex state keys are dropped, and the four hand-rolled
HostProvider.workspace test stubs are consolidated into one shared
helper.

* test(vscode): add e2e coverage for workspace-scoped hook discovery

Boots real VS Code with the packaged extension against the workspace
fixture, sends a prompt, and asserts the fixture's UserPromptSubmit hook
was discovered from the open window's workspace, executed with that
workspace root as its cwd, and received the same root in its
workspaceRoots input — the end-to-end contract the hook workspace
identity fix establishes.

* test(vscode): isolate the e2e hook fixture from the shared workspace

The UserPromptSubmit fixture hook lived in the shared e2e workspace, so
every prompt-sending spec executed it (hooksEnabled defaults to true) —
and its cold PowerShell spawn on Windows pushed chat.test.ts past the
5s expect timeout. hooks.test.ts now overrides workspaceDir to a
dedicated workspace-hooks fixture, so only the hooks spec pays the hook
spawn.

* fix(hub): cap hub-events db size so it can't fill the disk (#13516)

* fix(hub): cap hub-events db size so it can't fill the disk

Row/time retention alone didn't bound disk usage: envelopes carrying
full session snapshots reach hundreds of KB each, so retained rows
could total tens of GB, sweeps only ran hourly, and DELETE never
shrinks a SQLite file. Enforce a 64 MiB size budget in prune() (oldest
rows first, VACUUM to return the space), and also prune after every
16 MiB appended so bursts can't outrun the hourly timer.

Fixes #13505

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

* fix(hub): tolerate VACUUM failure on a full disk

VACUUM needs scratch space and can fail in exactly the state a
ballooned event log causes. The byte-budget deletes already bound live
data, so swallow the error and let the next sweep retry the reclaim
instead of aborting startup pruning and disabling the durable log.

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

* fix(hub): count the size budget in UTF-8 bytes, not characters

envelopeJson.length (UTF-16 units) and SQLite LENGTH() (characters)
undercount multibyte text by up to 3x, which could leave a CJK-heavy
log settled above budget and re-running VACUUM every sweep. Use
Buffer.byteLength and LENGTH(CAST(... AS BLOB)) instead.

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

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(sdk): carry root overrides into the Node smoke-test sandbox (#13517)

ci-node-smoke.ts installs the packed SDK tarballs with a plain npm
install in a fresh temp dir, where the repo root package.json overrides
do not apply. When @sap-cloud-sdk 4.9.0 shipped (2026-08-24) it broke
@sap-ai-sdk/ai-api 2.14.0 (via @jerome-benoit/sap-ai-provider in
@cline/llms) with ERR_PACKAGE_PATH_NOT_EXPORTED, failing the smoke step
on every PR even though the root already pins @sap-cloud-sdk/* to 4.6.0.

Copy the root overrides block into the generated sandbox package.json
so the smoke install resolves the same pinned versions as the repo and
future third-party releases cannot break it independently.

* chore(sdk): release v0.0.79

* fix(vscode): don't steal last-used provider from ClinePass on credential refresh (#13520)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): flush the /shutdown 202 before daemon teardown

The /shutdown handler queued teardown on a microtask, which runs before
the event loop's write phase, so the daemon could process.exit() before
the accepted 202 was handed to the socket. Unix masked it (uv_try_write
lands small loopback writes synchronously); Windows has no such fast
path and lost the race regularly — the recurring shutdown.e2e.test.ts
'socket hang up' failures on windows-latest. Start teardown from the
response's write callback instead, with an idempotent 1s fallback so a
client that vanishes mid-write cannot strand the daemon, and send
Connection: close so the client gets a FIN rather than an abort.

Since the flakiness this compensated for is fixed at the source, restore
maxWorkers: 2 for the Windows core suite (serializing it cost ~3 min of
CI per run), and raise the e2e daemon discovery hang guard 10s→30s —
it guards against hangs, not runner speed.

* chore(cli): release v3.0.58

* fix(core): prevent search_codebase from crashing the process on giant single-line files (#13525)

* fix(core): prevent search_codebase from crashing the process on giant single-line files

searchWithRipgrep buffered all of rg's --json stdout into one string. Each
JSON event embeds the full text of the matched line (--max-columns is
ignored in JSON mode), so searching a directory of serialized trace dumps
(single-line multi-hundred-MB JSON files) accumulated gigabytes of stdout
until string concatenation threw RangeError: Out of memory inside the
stream data handler. That throw is outside the tool's try/catch, so it
escalated to an uncaughtException and killed the CLI/hub daemon.

Parse rg's JSON events incrementally line by line, drop events larger
than 256KB, truncate matched/context lines to MAX_LINE_CHARS, and stop
reading once maxResults is reached. The fallback regex scan now skips
files larger than 10MB (reporting the skip count) and truncates its
context lines the same way.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* simplify search_codebase crash fix to a minimal diff

Replace the incremental JSON-event parser with three small guards: stop
buffering rg stdout past 10MB, drop the trailing partial event before
parsing, and slice fallback context lines to MAX_LINE_CHARS. Drops the
fallback file-size skip and skip-count reporting.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag (#13522)

* chore(vscode): remove per-tool MCP auto-approve checkboxes from webview

MCP auto-approval is now governed solely by the global 'Use MCP servers'
toggle; the SDK approval path (shared with the CLI and desktop app) has no
per-tool granularity, so the per-tool and 'Auto-approve all tools'
checkboxes were no-ops that implied control that no longer exists. Remove
them from the MCP settings view and chat tool rows. The autoApprove arrays
in cline_mcp_settings.json and the toggleToolAutoApprove RPC are left
intact for the legacy extension.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag

Keep the checkbox components, handlers, and RPC plumbing intact but gate
rendering behind SHOW_MCP_PER_TOOL_AUTO_APPROVE=false: the SDK approval
path (shared with the CLI and desktop app) is all-or-nothing via the
global 'Use MCP servers' toggle, so the per-tool checkboxes were no-ops.
Flip the flag back on if the SDK gains per-tool approval granularity.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(tools): create new files with the platform-native line ending (#13521)

* fix(tools): use platform-native EOL for new files and preserve CRLF in apply_patch updates

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* simplify to the minimal new-file EOL fix

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* extract shared normalizeNewFileLineEndings helper

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add suggested schedule templates to the desktop Schedules page (#13529)

* Add suggested schedule templates to desktop Schedules page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix unreadable selected text in inputs caused by selection utility conflict

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Restyle Suggested section label as small gray uppercase

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Hide suggested schedule cards that match an existing schedule name

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Disable the agent todo tool and hide the Agenda UI in the desktop app (#13530)

* remove todo tool and Agenda UI, keep schedule-only tasks tool

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: biome formatting fixes

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* restore agenda backend; disable todo kind behind a flag instead of deleting

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* keep agenda automation pump idle while the todo tool is disabled

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* remove todo tool and Agenda UI altogether (revert the disable-flag hybrid)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* restore all agenda code to main state

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* disable agent todo tool and hide Agenda UI behind flags

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add Desktop App and Cloud Platform to bug report issue template (#13532)

* Add Desktop App and Cloud Platform to bug report surfaces

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename Surface Diagnostics field to Diagnostics

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: sidebar navigation cleanup with New/Schedule/Customize rows and dialog-based search (#13533)

* desktop: clean up sidebar navigation chrome

- Give New Task its own full-width labeled row below the logo row
  instead of an ambiguous icon next to the agenda toggle
- Wire the New Task row to the home action so starting a new task
  clearly takes you home (the logo still works as a fallback)
- Swap back/forward chevrons for browser-style arrow icons and
  bump their size

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: sidebar New/Schedule/Customize rows and always-visible search

- Stack New (plus icon), Schedule, and Customize as full-width labeled
  rows below the logo; whole row highlights on hover via sidebarItem
- New starts a fresh task (home), Schedule opens Settings > Schedules,
  Customize opens the Customizations sections (Plugins first)
- Show the session search bar permanently above the sessions list
  instead of hiding it behind a search icon toggle

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: move session search into a dialog behind a logo-row icon

- Replace the inline sidebar search bar with a search icon in the
  logo row that opens a cmdk command dialog listing sessions
- Selecting a result opens that session and closes the dialog
- Remove the agenda/tasks toggle the icon replaces, along with the
  now-unreachable sidebar Agenda panel (the welcome screen still
  surfaces agenda tasks)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: load full session history when the search dialog opens

Addresses Greptile review on #13533: the dialog only searched the
currently loaded history batch, so older unloaded sessions could not
be found. Opening search now kicks off loadAllSessions() (the hook's
purpose-built global-search loader), and the empty state reads
'Searching older sessions...' while more history is streaming in.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Hide Channels and Agents sections from desktop app sidebar (#13527)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop app: organize sidebar sessions into Pinned, Scheduled, and Tasks sections (#13528)

* Add Pinned/Scheduled/Tasks categories to desktop app sidebar

Replace the Schedules and Favorites filter-menu options with visible
collapsible category sections in the session sidebar, and rename the
Favorite action to Pin across the sidebar and sessions view.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Grow full history window when Tasks show-more outpaces loaded tasks

loadMoreSessions treats its argument as a limit on all sessions, but the
Tasks show-more count only tracks Task rows, so once pinned/scheduled
rows pushed the loaded total past the requested count the call no-oped
and clicks went dead. Grow the whole history window via
loadOlderSessions instead, and only when the loaded tasks cannot fill
the next page. Addresses Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Auto-fill the Tasks page instead of fetching once per show-more click

A single 50-session window growth can consist entirely of pinned or
scheduled sessions, leaving a show-more click with no visible Tasks
progress. Replace the one-shot fetch with a page-fill effect that keeps
growing the history window until the requested Tasks page fills or
history runs out. Addresses the follow-up Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Halt page-fill retries after a failed history fetch

A failed fetch leaves the task count and has-more state unchanged,
which are exactly the conditions the page-fill effect fires on, so one
failing request would retry and re-toast forever. Halt the effect after
a failure and let the next explicit show-more click retry. Addresses
the third Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Redesign desktop Model Providers page and split voice input into its own settings page (#13531)

* Redesign desktop Model Providers page and split voice input into its own settings page

- Group providers into Connected / Popular / All with auth-kind hints and
  connection status instead of per-row enable toggles
- Show browser sign-in (not an API key field) for OAuth providers, with a
  collapsed manual-key escape hatch where supported, plus explicit
  Connect / Disconnect / Sign out actions
- Move voice input to a dedicated Settings > Voice page that only offers
  connected transcription-capable providers, preselects a default model
  (streaming preferred), and stays disabled in the sidebar until a
  provider is connected

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Show native tooltip on the disabled Voice settings nav item

Disabled buttons drop pointer events, so the 'connect a model provider'
hint moves to a wrapping span for the browser tooltip to render.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Drop letter avatars and gray provider ids from provider rows and voice chips

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Drop model counts from provider list rows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename provider Connected status to Configured and drop the green styling

A settings entry is configuration, not a live connection; neutral gray
text avoids implying an active link, since the user still picks which
configured provider to use per chat.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Resync provider catalog from disk when a settings save fails

Connect/disconnect/credential edits update the list optimistically; a
failed save now reloads the catalog instead of leaving the optimistic
state (and the view's module cache) claiming a configuration that was
never persisted.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename oauthProvider test fixture to dodge CodeQL name heuristic

CodeQL's clear-text-storage query flags any identifier matching 'oauth'
as a credential source and traced the fixture's provider id into the
favorite-models localStorage write, which stores only provider/model id
strings. Renaming the fixture removes the false-positive source.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Guard catalog reloads against races and resync detail drafts on failed saves

Optimistic provider mutations now bump a generation that discards any
in-flight catalog response, so a failed-save recovery reload can't
overwrite a newer edit with an older disk snapshot. The recovery also
remounts the provider detail panel via a reset token so its local field
drafts reflect the reloaded on-disk state instead of unpersisted edits
or an optimistically cleared disconnect.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix failed-save recovery ordering and retry superseded reloads

Remount the provider detail only after the authoritative catalog reload
lands, so its drafts re-seed from disk state rather than the optimistic
values that failed to persist. When a concurrent edit supersedes the
recovery's in-flight response, retry the reload (bounded) instead of
dropping it, since that edit performs no reload of its own.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): Customize hub, sidebar overhaul, and settings polish (#13538)

* feat(desktop): merge customization pages into a Customize hub with inline marketplace

Replaces the Plugins page and the dedicated Marketplace page with a single
Customize hub. Tabs: Skills, MCP, Plugins, Rules, Hooks, Tools, each with
live counts. Tabs backed by a marketplace catalog render the installed
items followed by an inline browsable Browse section (CLI-hub style), so
installing from the catalog immediately reflects in Installed above.

- Installed cards restyled to mirror the browse-card anatomy: bg-card p-4
  containers, absolute top-right xs Uninstall matching Install, truncating
  semibold titles, primary-tinted icons, real Badge components instead of
  ad-hoc bordered spans, un-indented line-clamped descriptions
- Rules/Hooks/Tools rows brought into the same card language; redundant
  intro paragraphs (duplicating the page description) removed; Tools group
  headers match the Installed header style with counts
- Marketplace section header renamed to Browse; duplicate 'N results' row
  removed (the header count is the single source)
- MCP embedded view now shows the full marketplace instead of
  installed-only

* feat(desktop): overhaul sidebar sessions and navigation

Sessions list:
- Sort toggle removed; sessions are always grouped by project, with pinned
  sessions leading each group (both subsets ordered by recency). The
  Pinned/Scheduled/Tasks category sections and their time-mode paging
  machinery (page-fill effect included) are deleted
- Scheduled sessions get an inline clock icon next to the pin position;
  pin + clock render together when both apply, and the running/unread
  status dot now coexists with them
- One font size (text-sm) across the list: titles, timestamps, project
  headers, show-more buttons, empty states. sidebarText needed !text-sm
  because the default button size's text-base wins the twMerge conflict
- Gradient fade under the Sessions header once the list scrolls, so rows
  fade out instead of hard-clipping
- The session-detail hover card is controlled from the sidebar and closes
  on scroll (Radix receives no pointer events while scrolling, so it used
  to float over moving content)
- Sidebar min resize width raised 224->260px; the per-project show-more
  label truncates so its nowrap text can't force rows to overflow and clip
  timestamps at narrow widths

Navigation:
- Customize replaces the Plugins/Marketplace/Hooks/Rules/Tools sidebar
  entries; Schedules and Customize are hidden from the expanded settings
  nav (their top rows cover them) but stay reachable when collapsed
- The settings gear always opens General instead of resuming the last
  section; the Account no-op hover special case is gone
- The New row highlights (aria-current) while the fresh not-yet-started
  task page is showing and hands off to the session row once the task
  starts; hitting New also focuses the prompt input via a window-event
  signal (lib/prompt-input-focus.ts) since the sidebar and composer sit in
  distant subtrees
- Fixed the xs button size collapsing any icon-bearing button to 12x12
  (leftover has-[>svg]:size-3 from when xs was a micro button) — this was
  why Uninstall buttons rendered broken next to Install

* feat(desktop): polish settings pages and chat composer

Models page:
- The provider detail panel is always open: no X button, no empty
  no-selection state. It defaults to the first connected provider (falling
  back to the first in the catalog), which also removes the layout shift
  that happened when the page swapped between full-width and panel
  variants on selection
- Fixed the list pane becoming unscrollable while the panel was open:
  grid items default to min-size auto, so the pane grew past its track
  inside the overflow-hidden grid and its ScrollArea had nothing to
  scroll; wrapped it in a min-h-0 min-w-0 cell
- Add Provider opens a Dialog instead of swapping the page
  (AddProviderContent gained a dialog variant that renders only the form)
- Embedded inputs (provider search, model search, detail fields) share one
  EMBEDDED_INPUT_CLASS stripping the Input component's own border/dark bg
  tint/shadow/ring, which rendered as a mismatched inner box; the model
  search box uses the same h-9/px-3 frame as the provider search
- Model list flows with the page instead of a max-h capped inner scroller

Other pages:
- Account uses the shared PageFrame/PageHeader: left-aligned, text-3xl
  title, Sign Out in the header actions slot
- Desktop notifications is one General section: header row plus the
  Event/Notify/Sound matrix nested in a card, so its rows no longer read
  as top-level peers of Dark mode; 'Available in the desktop app' label
  removed
- Schedule page retitled from Schedules with a real description; Customize
  description rewritten

Chat composer:
- The voice dictation button only renders once a voice model is
  configured (Settings -> Voice); the unconfigured deep-link state is
  gone (prop type kept for an easy restore)

* chore(desktop): release v0.0.17

* fix(desktop): unblock sdk-test lint on the voice-input model picker (#13553)

The model picker renders a radiogroup of styled buttons with role=radio
and aria-checked; biome's useSemanticElements flags the role as an
error, which fails the sdk-test Quality Checks lint for every PR
touching sdk/ or apps/ paths. Suppress with a justification — switching
to input type=radio needs a restyle and belongs to the desktop settings
work.

* fix(vscode): include rich workspace metadata in system prompt (#13518)

* capture richer workspace information for vs code extension

* fix(shared): redact credentials from workspace remotes

* fix(shared): avoid regex backtracking in remote redaction

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>

* Hide task costs on vscode when ClinePass is selected (#13515)

* fix: stop showing cost estimates for subscription-billed providers (#13552)

* fix(vscode): stop showing cost estimates for subscription-billed providers

Providers whose usage is covered by a flat-rate subscription (ChatGPT
Plus/Pro via openai-codex, ClinePass) are marked with
metadata.usageCostDisplay = "subscription" in the SDK, and the CLI
already suppresses dollar figures for them. The VS Code host collapsed
that value into "show" before it reached the webview, so the task
header and model pricing rows rendered API-rate cost estimates that
users read as real charges on top of their subscription.

Pass all three usageCostDisplay values ("show" | "hide" |
"subscription") through the catalog listing and render cost only when
the value is "show", matching the CLI's shouldShowCliUsageCost
policy.

* feat(llms): mark Claude Code as a subscription-billed provider

Claude Code is typically authenticated with a Claude Pro/Max
subscription, but its models reuse Anthropic API pricing metadata, so
Cline rendered per-token prices and API-rate cost estimates for usage
that is covered by the subscription. Set usageCostDisplay =
"subscription" on the provider (picked up by the CLI and the VS Code
webview) and suppress the price rows in the Claude Code settings card.

The Claude Code CLI can also run on API-key billing, where a real cost
exists; the provider cannot distinguish the two, so we prefer showing
no number over a misleading one.

* fix(vscode): suppress cost display until provider listings load

While the ListProviders request is in flight (or after it fails), the
usage-cost hook had no listing to consult and fell back to "show",
flashing the API-rate estimate at subscription users on every chat-view
mount — the exact display the previous commit removes. Return
"unknown" whenever listings are absent; consumers already render cost
only for "show", so they suppress it during that window with no
changes. Briefly hiding a real cost is harmless, briefly showing a fake
charge is not.

* fix(desktop): reconcile voice settings after main sync

* test(llms): allow experimental ElevenLabs models

* fix(sdk): preserve canonical media model behavior

* feat(desktop): customize macOS DMG install window (#13563)

* feat(desktop): add Retina DMG background tooling

* feat(desktop): customize the macOS DMG layout

* ci(desktop): validate DMG background assets

* fix(desktop): adjust DMG Applications icon position

* ci(desktop): drop redundant DMG artwork validation from publish workflow

Tauri's beforeBuildCommand already runs dmg:background (with its own
validation) at the start of the build/sign/notarize step, and the
release/beta config overlays do not override the build section, so this
step duplicated work the publish job performs anyway. PR-time coverage
lives in desktop-test.yml.

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>

* feat(desktop): sidebar time view, Customize/Marketplace split, and schedule page UX (#13570)

* feat(desktop): split Customize into Installed and Marketplace pages

The Customize hub previously embedded a Browse section inside every tab
that had a catalog. That inlining made each tab long and buried the
catalog. Customize is now the installed inventory only (skills, MCP,
plugins, rules, hooks, tools tabs pass marketplaceVariant="installed"
to the embedded MarketplaceView; McpServersContent grew the same prop),
with an outline Marketplace button in the header.

Browsing moved to a dedicated Marketplace settings section that renders
the previously dead "directory" variant of MarketplaceView: one list
across all catalog types with type-filter chips, wrapping tag chips,
and light rules separating the filter tiers from each other and from
the results. The Clear control now renders inline at the end of the tag
row only while a tag is active, the Updated date is gone, and the
header hosts an Installed button mirroring the one on the Customize
page. Directory subheader copy: "A curated set of plugins, MCP
servers, and skills from the Cline community."

Tag and type chips wrap to new lines instead of scrolling
horizontally.

* feat(desktop): sidebar time view with sections, sort toggle, and scheduled detection

Restores the time-sorted session list as the default sidebar view, with
collapsible Pinned / Scheduled / Tasks sections (headers appear only
once something is pinned or scheduled) and the page-fill effect that
grows the fetched history window until a Show-more click makes visible
progress. Project grouping stays as the alternate mode behind a
one-click sort toggle whose icon reflects the active mode — the old
dropdown cost an extra click for a two-option choice.

Scheduled sessions are detected two ways: the hub-schedule origin
trigger in session metadata, plus a fallback that asks the hub which
session ids belong to schedule executions (list_routine_schedules,
fetched on mount and every two minutes, merged into a rolling set).
The fallback matters because locally executed scheduled runs do not
reliably stamp the trigger into session metadata — a real scheduled
session created today carried only {mode:"user"} provenance. The
scheduled clock icon now leads the row, left of the title; pin and
timestamp stay on the right.

The initial visible page grows from 10 to 30 rows so a tall sidebar
fills instead of stranding a stub of rows over empty space (history
fetches already start at 50).

The expanded sidebar's Customize row now hosts indented Installed and
Marketplace sub-tabs while a customize section is open; the active
sub-tab carries the full selected background while the parent keeps a
subtler one so the two simultaneous highlights read differently.

Also fixes the hover-card flash on click (logo card and session-row
cards): Radix HoverCardContent sits on a DismissableLayer, so a click
on the trigger registers as a pointer-down outside the card and
dismisses it, and the trigger's focus event immediately reopens it.
onPointerDownOutside preventDefault suppresses the dismissal; cards
still close on pointer leave.

* feat(desktop): schedule page row, dialog, and details UX polish

Schedule cards are now click targets: clicking anywhere on a card
outside its controls opens the details dialog (guarded via
closest("button,...") since every inline control, including the Radix
switch, renders a button element), with Enter/Space keyboard support.
The redundant eye button is gone. The remaining edit / run / pause /
delete buttons grow from the 12px icon-sm size to 28px targets with
16px icons, sized consistently with the adjacent enable toggle — the
icons use explicit size-4 classes so the Button base svg rule cannot
shrink them back.

The new/edit dialog gains breathing room between field labels and their
inputs (space-y-2 per field wrapper).

The details dialog no longer scrolls as a whole when the schedule JSON
is long: the dialog is a flex column capped at 85vh, the JSON pre
shrinks to the remaining space (min-h-0) and scrolls internally, and
the Runs tab list scrolls inside the tab the same way.

* feat(desktop): scheduled sessions UX — unified details dialog, run-now handoff, hidden steering, stuck-thinking fix (#13573)

* feat(desktop): merge schedule details into one view and open run-now sessions

The schedule details dialog drops its Overview/Runs tabs: one scrollable
column with the meta grid, the configuration JSON (capped at max-h-64
with internal scroll so it cannot crowd out what follows), and a Runs
section beneath it showing the three most recent runs with a ghost
"Show all N runs" expander (collapsed again whenever a different
schedule's details open). The "Full configuration for this schedule"
subtext is gone; the dialog passes aria-describedby={undefined} so
Radix does not warn about the missing description.

Run now hands you into the session it starts. The trigger command
queues the run and returns before the runner attaches a session id, so
after the toast the handler polls the schedule overview once a second
for up to 15 seconds — which doubles as keeping the page's run status
fresh (refreshSchedules now returns the fetched overview to make that
single-stream) — until the triggered execution reports its session id,
then calls onOpenSession. Guarded so it never auto-navigates after the
user left the page.

* feat(desktop): hide runtime steering messages from transcripts

Scheduled/automation runs inject user-role steering messages each
iteration ("[SYSTEM] This run is not complete until you call
submit_and_exit...", plus a team-obligations variant). The chat view
rendered them as user bubbles, as if the person had typed them — in a
scheduled session the transcript was mostly [SYSTEM] noise.

They are machinery talking to the model, not something the person said
or needs to read, so the transcript now hides them entirely:
MessageBubble renders null for any [SYSTEM]-prefixed user message.
Grouping still treats them as working-row machinery via a single
isSystemSteeringMessage predicate — they collapse into the run's work
span, are never a turn boundary, can never be mistaken for a run's
answer, and never advance the run count even when metadata is missing —
so work-block folding and checkpoint/edit run numbering stay correct.
A finished scheduled session now reads as prompt, work summary, answer.

* fix(desktop): poll history while an attached session's event stream is dead

Opening a scheduled session while (or right after) it runs left the
view stuck on the thinking shimmer until the user switched away and
back. Root cause is in core: the hub daemon executes scheduled runs on
a private LocalRuntimeHost inside createLocalHubScheduleRuntimeHandlers,
while the hub server only projects live events from its own session
host — so session.attach succeeds but no assistant/tool/status events
ever flow. And since multiple hub daemons share cron.db, a run claimed
by a different daemon is invisible to this hub regardless. The proper
core rewiring is tracked as ENG-2474.

Client-side heal that covers every case: while an attached history
session reports a busy status and no chat_event chunk has arrived for
five seconds (and no assistant bubble is mid-stream), poll every three
seconds — re-read canonical history, merged through the same dedupe
path hydration uses, and the session record's status — so the
transcript and the thinking indicator settle in place. Locally driven
turns keep chunks flowing, so the quiet-window guard keeps the fallback
inert there.

* chore(desktop): format workspace selector components

Biome formatting drift that landed on main; picked up by a formatter
pass over components/views/chat.

* fix(desktop): keep stale-stream poll inert during locally driven turns

The fallback poll could fire between a local submit and the model's
first chunk (optimistic user bubble added, stream quiet past the
window, no assistant bubble yet). It then replaced the optimistic
bubble — raw prompt text — with its canonical history twin, which is
stored wrapped in a user_input envelope. The rekey handler that runs
when the stream starts looks for a trailing user bubble matching the
raw prompt, finds only the wrapped copy, and appends a second bubble:
duplicated messages in normal interactive chat.

The poll now stays inert while a local turn is in flight
(turnEpoch !== turnSettledEpoch, or outstanding optimistic user
messages), checked both before polling and again after the snapshot
returns. Hydration marks the turn settled — the mount defaults
(epoch 0, settled -1) otherwise read as an open turn and would keep
the fallback inert forever for the scheduled-session case it exists
for. Applying a polled snapshot also rebuilds the live tool routing
keys, same as hydration, so later tool events update canonical rows
in place instead of appending.

* fix(desktop): keep the working indicator alive for narrating scheduled runs

Watching a scheduled run live: the first tool row appeared, then the
thinking indicator vanished with nothing streaming, and the rest of
the run (final answer, submit_and_exit) only showed up seconds later
in one lump.

inferHydratedChatStatus treats a "running" session record whose
transcript ends on an assistant message as a session that died without
a status flip and reports "completed". That heuristic is right for
stale records, but scheduled/automation models narrate between tool
calls, so a polled snapshot can genuinely end on assistant text
mid-run — the completed flip hid the working indicator, folded the
run early, and disarmed the stale-stream poll (status left the busy
set), dead-ending live updates until an in-flight poll happened to
deliver the finished run.

The heuristic now only applies once the transcript has actually gone
quiet (newest message older than two minutes — comfortably past model
latency plus tool runs). A recently active transcript keeps the
record's "running" verdict, so the indicator stays up and polling
stays armed until the record itself settles.

* fix(desktop): stale-stream poll mirrors the session record instead of inferring

Replaces the previous fix for the vanishing working indicator (the
time-window guard added to inferHydratedChatStatus) with a version
that adds no inference at all: the heuristic is restored to exactly
its long-standing form, and the poll now maps the session record's
status verbatim (mapSessionRecordStatus).

The record is the right authority in the poll's context: the sessions
this fallback serves have a live host maintaining their record, and it
flips to a terminal status when the run ends. Transcript-shape
inference belongs only where it has always lived — hydrating sessions
whose records may be orphaned — and would misread a mid-run snapshot
ending on assistant narration as a finished session, hiding the
working indicator and disarming the poll.

* fix(desktop): address review findings on steering detection and run-now matching

Steering detection additionally requires the injected-message marker
(meta.userRunSpan === 0) beside the [SYSTEM] prefix, so a person's
genuine prompt that happens to start with "[SYSTEM]" stays visible
and turn-counted. The failure direction is deliberate: an unstamped
injected reminder would merely show as a user bubble, while the
content-only check could hide a real prompt.

Run-now only follows the execution id the trigger reply itself named;
the newest-execution-for-this-schedule fallback could open a previous
run's session when the trigger failed to enqueue one.

* fix(desktop): report a failed run-now instead of confirming a start

A trigger reply without an execution means no run was enqueued (the
schedule may have been disabled or deleted since the page loaded). The
handler previously toasted "Run started" regardless and then silently
skipped the session-open polling. It now shows a destructive
"Run not started" toast, refreshes the schedule list so the row
reflects reality, and skips the polling entirely.

* fix(desktop): don't block the main thread on quit while stopping the sidecar (#13566)

Quitting the mac app beach-balled for ~5-7s. The shutdown POST was
built from the ws transport URL (appending /shutdown lands inside the
query string), so the sidecar was never told to exit, and stop() then
polled the child for up to 7s on the main thread - on macOS inside
applicationWillTerminate - before SIGKILLing it.

stop() now sends SIGTERM and returns immediately. The sidecar handles
SIGTERM with the same bounded (5s) graceful shutdown as the /shutdown
endpoint and exits itself, finishing session persistence as an orphan.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): hover trash button on sidebar session rows (#13582)

Each session row shows a trash icon on the right while hovered (or
when the button itself is focused), opening the same delete
confirmation dialog the row's context menu uses. The row is a button
and buttons cannot nest, so the trash is an absolutely positioned
sibling inside a group/row wrapper, overlaid where the timestamp sits:
row hover hides the timestamp, shows the trash, and moves the row's
hover background to the wrapper group so it holds while the pointer is
on the trash itself.

* fix(desktop): install marketplace plugins and MCP servers in-process instead of spawning a cline binary (#13585)

* fix: install marketplace plugins and MCP servers in-process instead of spawning a cline binary

The desktop app sidecar and cline-hub shelled out to 'cline plugin install'
and 'cline mcp install' for marketplace installs. Packaged GUI apps inherit
launchd's minimal PATH on macOS and most desktop users have no cline CLI
installed at all, so installs failed with a red
'Executable not found in $PATH: "cline"' error.

Install via @cline/core's installPlugin/installMcpServer in-process instead,
matching what the VS Code extension already does. Also fix
parseMcpInstallArgs in @cline/core to treat the marketplace catalog's '--'
separator as end-of-options; previously the separator itself became the
stdio command.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop test-injection plumbing from marketplace installers

Call @cline/core's installPlugin directly instead of threading an
installer option through the marketplace entry points; tests stub the
core module instead.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert: keep cline-hub marketplace installs CLI-backed

The hub dashboard is launched via 'cline dashboard', so a CLI is always
present and CLINE_WRAPPER_PATH resolves it; the PATH bug only affects
the desktop app, which does not ship a CLI.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.18

* chore(vscode): release v4.1.16

* chore(sdk): release v0.0.80

* chore(cli): release v3.0.59

* fix(hub): stop shipping full transcripts inside broadcast hub events (#13587)

* fix(hub): stop shipping full transcripts inside broadcast hub events

Every session.updated (and session.created/detached/run.started) event
embedded the session's ENTIRE message transcript via readCoreSessionSnapshot,
even though no consumer reads snapshot.messages off an event — clients fetch
messages with the session.messages command. For a multi-megabyte transcript
this turns every status flip into megabytes per subscriber, floods the
durable event log, and (until the send-queue backpressure fix lands) lets a
slow subscriber balloon the hub process by one full transcript copy per
event — reported as a 25GB cline process on a 16GB Mac.

Strip snapshot.messages centrally in HubServerTransport.publish() so every
current and future event publisher is covered, the event log stores slim
envelopes, and cursor replay stays byte-identical with live fan-out. All
other snapshot fields (status, usage, model, workspace, checkpoint) are kept,
and command replies are untouched.

* fix(hub): never capture the transcript into event/reply snapshots

Replaces the publish-boundary strip with the real fix: don't build
message-bearing snapshots in the first place. emitSessionSnapshot no longer
re-reads the entire transcript from disk on every status flip, and
readCoreSessionSnapshot no longer reads it for any event or reply — a
snapshot is a state notification (status, usage, model, workspace,
checkpoint); the transcript is fetched via the session.messages command.
Checkpoint-restore snapshots (session-versioning-service) are untouched:
restore replies carry messages in their own dedicated field.

* chore(desktop): release v0.0.19

* chore(sdk): release v0.0.81

* chore(cli): release v3.0.60

* fix(vscode): avoid render crash on malformed api_req payloads in combineApiRequests (#13560)

* fix(vscode): stop pinning DeepSeek model count in catalog smoke test (#13600)

* feat(ui): share agent welcome hero (#13567)

* feat(ui): share agent welcome hero

* test(ui): cover welcome hero pointer states

* refactor(ui): keep welcome hero API minimal

* test(ui): verify welcome hero package assets

* fix(ui): inline welcome hero masks

* fix(tools): preserve a file's own CRLF line endings across apply_patch updates (#13512)

* fix(desktop): keep the window title bar draggable across views (#13572)

* fix(desktop): keep window title bar persistent

* fix(desktop): reserve persistent title bar space

* fix(desktop): polish persistent title bar layout

* Sign Windows CLI binaries with Azure Trusted Signing; surface app-control launch errors (#13021)

* feat(cli): sign Windows binaries with Azure Trusted Signing and surface app-control launch errors

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): use _CLI-suffixed signing profile secret, normalize endpoint, fail loud on partial config

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* Tunnel ProtoBus over the existing Host Bridge (#13218)

* feat(core): tunnel ProtoBus over Host Bridge

* fix(core): harden Host Bridge stream lifecycle

* fix(core): serialize concurrent chunked responses per request

Streaming handlers deliver updates fire-and-forget, so two logical
responses for one request_id can be in flight at once. Chunked payloads
made forwarding non-atomic: each chunk write is an await, so concurrent
forwards could interleave their chunk sequences and the receiver --
which reassembles purely by arrival order -- would splice two payloads
into one. Route all forwards for a request through one promise chain; a
failed write rejects every later forward so a torn payload is never
followed by more chunks.

Rename the lock manager's instanceAddress to instanceOwner: it holds an
opaque per-spawn instance ID on the token path and a listener address
only on the CLI-harness path. Delete the caller-less getInstanceByPort
query that interpreted the owner as an address.

Also: document message_json as a legal wire encoding for small
payloads, close the gRPC client when startup fails, note the
intentional discard of the cancellation confirmation, and add the
proto's trailing newline.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* Build and Authenticode-sign a Windows x64 desktop installer in desktop releases (#13607)

* feat(desktop): build and Authenticode-sign a Windows x64 NSIS installer in desktop releases

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): pin OIDC-adjacent actions to commit SHAs in the Windows signing job

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): pin checkout and upload-artifact to commit SHAs in the Windows signing job

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): show agent-created schedules on the Schedules page (#13613)

* fix(desktop): show agent-created schedules on the Schedules page

Schedule hub commands are scoped to the workspace registered by the
connection, but the desktop app's hub client registers the app launch
directory while agent-created schedules live under each chat's own
workspace folder - so they never appeared on the Schedules page.

Grant token-authenticated hub connections (which can already bind any
workspace at registration) explicit cross-workspace schedule access via
an allWorkspaces payload flag, and have the desktop sidecar request it
for routine schedule commands. Workspace-bound clients (local browser
origins) and default CLI behavior stay scoped.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(core): strip allWorkspaces flag from schedule inputs and pin it in the sidecar payload

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make suggested routine template prompts prescriptive about their final output (#13611)

* Make bug hunter routine template prescriptive about its final report

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make remaining routine templates prescriptive about their final output

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop: surface scheduled-task final output — auto-expand submit_and_exit and render its summary as markdown (#13612)

* desktop: auto-expand submit_and_exit and render its summary as markdown

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: render submit summary in full foreground color

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: label the submit row 'Scheduled task completed'

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: label errored submit_and_exit rows as failed

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add tooltips explaining Live and After recording badges on voice input models (#13610)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Remove box shadow from chat message actions row (#13630)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): make the Tauri shell work on Windows (#13632)

- Defer updater installation to the user-initiated restart on Windows:
  install() launches the NSIS installer and exits the process immediately,
  so the background cycle now downloads only and stages the bytes, and
  restart_to_apply_update installs them after stopping the sidecar.
- Spawn child processes (sidecar, git, cmd /C start) with CREATE_NO_WINDOW
  so the GUI-subsystem app doesn't pop visible console windows.
- Fall back to USERPROFILE when HOME is unset resolving the MCP settings
  path, matching the sidecar's homedir().
- Reap the sidecar after the Windows hard-kill so its exe file lock is
  released before the NSIS installer replaces it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop watching agenda spec dirs while the todo tool is disabled (#13629)

* fix(core): stop watching agenda spec dirs while the todo tool is disabled

Since #13530 disabled the agent todo tool, the Agenda UI, and the
automation pump, the hub still created fs.watch watchers on the global
agenda specs dir and on every workspace root recorded in the task store
(at startup and on scope access). Nothing consumes the watcher-driven
task events while the feature is off, and the task.* hub commands
already reconcile spec files on demand, so the watchers are pure
overhead - one OS watch handle per known workspace.

Wire watchFiles to AGENDA_TODO_TOOL_ENABLED the same way
automationEnabled is, preserving a host's explicit watchFiles opt-out
for when the flag is turned back on. Schedules are unaffected: the
schedule list has no file watcher and updates through hub commands and
published schedule events.

* fix(core): reconcile external spec edits inside updateTask

With the spec watchers off there is no background reconciliation, so a
task spec edited directly on disk made every same-store task.update fail
the signature check with "task spec changed outside the manager" until
an unrelated task.get or task.list happened to reconcile the scope.

Reconcile the task's scope at the start of updateTask (mirroring what
refreshAndVerifyTaskIntent already does for approve/run), skipping it
when the file reconciler itself is the caller to avoid recursing from
reconcileFileStore. An external edit now surfaces as the store's normal
stale-revision conflict, and a re-read-and-retry succeeds. This also
closes the pre-existing watcher debounce race for updates.

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently (#13565)

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently

Port the cline-provider refresh semantics to openai-codex and oca:
a transient refresh failure (network error, timeout, server 5xx) with an
already-expired access token now rethrows instead of returning null.
A null return means the refresh token was REJECTED and re-auth is
required; treating an outage blip as a rejection is what turned it into
a forced 'openai-codex requires re-authentication.' task stop while the
settings UI still showed the user as signed in.

Both providers also emit user.auth_refresh_soft_failure telemetry on
transient failures (the 'prevented logout' counter the cline provider
already has) and attach status/errorCode details to the genuine
invalid_grant logout event.

* refactor: collapse duplicate soft-failure telemetry branches and test

Review feedback: compute tokenExpired once and emit the soft-failure
event once in both providers, then return current credentials or
rethrow. Fold the codex soft-failure telemetry assertions into the
existing still-usable-token test instead of a near-duplicate case.

* fix: make OpenAI Codex (ChatGPT subscription) sign-in fail loudly instead of silently dead-ending (#13537)

* fix: make OpenAI Codex sign-in fail loudly instead of silently dead-ending

When callback port 1455 is already in use (e.g. by the Codex CLI or a
previous pending sign-in), startLocalOAuthServer returns a no-op server
and loginOpenAICodex would open the browser anyway, then dead-end:
the callback could never be received, and in the VS Code extension the
user just saw nothing happen after clicking 'Sign in to OpenAI Codex'.

- loginOpenAICodex now fails fast with an actionable 'port in use'
  error before opening the browser, unless the host provides manual
  code entry (the CLI's paste fallback keeps working)
- surface OAuth redirect errors (e.g. access_denied) instead of
  collapsing them into 'Missing authorization code'
- the extension dedupes concurrent sign-in clicks: a re-click re-opens
  the auth page of the pending flow instead of spawning a second flow
  that would collide with our own callback server
- browser-open failures now show an error message with the URL to
  open manually instead of only logging
- abandoned-flow timeouts no longer surface a confusing 'Missing
  authorization code' toast

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop host-side codex login dedupe, keep flow identical to CLI

The SDK owns the failure handling now (fail-fast on an unbindable
callback port), so the extension keeps the exact same simple
loginOpenAICodex call the CLI uses. A second click while a flow is
pending gets the SDK's clear port-in-use error, same as running
'cline auth openai-codex' twice would. Keep only the CLI-parallel
onOpenUrlError surfacing (the CLI prints 'open the URL above
manually'; the extension's equivalent is an error toast with the
URL).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(e2e): cover Codex sign-in callback-port failure and redirect errors

Two driven-VS Code tests for the OpenAI Codex (ChatGPT subscription)
sign-in flow:

- with port 1455 occupied on both loopback families, clicking the
  sign-in button surfaces the fail-fast port-in-use toast
- with the port free, the callback server binds and an OAuth redirect
  error (access_denied) propagates to a visible error toast

The second test opens a real browser tab to the OpenAI auth page as a
side effect of the genuine sign-in click.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* feat(core): anchor agent-created schedules in the user's .cline schedules home (#13634)

* feat(core): anchor agent-created schedules in the user's .cline schedules home

Agent-created schedules inherited whichever workspace folder the chat
session happened to run in, scattering user-level routines across chat
and project folders. They were invisible to workspace-scoped listings
elsewhere, tied to folders that may be cleaned up, and each chat's
tasks tool saw a different set when checking for duplicates.

Anchor them in ~/.cline/schedules instead: the hub's scheduled-task
session defaults now resolve to that home (created on demand), so
agent-created schedules live and run in one stable user-level scope.
The tasks tool guidance now tells agents that scheduled sessions run in
the schedules home, so prompts must carry absolute paths to any project
they operate on.

Schedules created explicitly with a workspace (CLI --workspace, desktop
routine wizard) are unchanged, and existing rows keep their current
workspaceRoot - they stay visible through the all-workspaces listing
paths (#13613, #13633).

* test(core): restore any pre-existing CLINE_DIR after the agenda hub test

The test's cleanup deleted CLINE_DIR outright, so an environment that
had it configured would leave later tests in the same worker on the
default storage directory. Save the previous value and restore it.

* test(core): restore CLINE_DIR even when hub test setup throws early

Restoring the override in the try/finally missed failures thrown during
transport construction or start(), before the try was entered. Register
the restore with onTestFinished instead, which runs regardless of where
the test fails.

* fix(desktop): don't show providers as configured without real credentials (#13608)

* fix(desktop): don't show providers as configured without real credentials

The desktop settings marked any provider with a persisted settings entry
as Configured, but legacy VS Code migration and empty saves can seed
entries (e.g. qwen-code, sapaicore) holding only a default model and no
credentials. Move the CLI's isProviderSettingsUsable readiness check into
@cline/core, expose it as a computed 'configured' flag on the provider
catalog, and use it in the desktop's isProviderConnected.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): resync catalog after saves so Configured badge updates live

Optimistic provider mutations can't know the sidecar-computed 'configured'
flag, so after connecting a keyless provider or saving cloud credentials
(e.g. a Vertex project id) the row stayed 'Not configured' until remount.
Silently refetch the catalog after each successful save, guarded by the
existing generation counter so newer edits discard stale responses.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): claim a generation in post-save resync so overlapping refreshes can't apply stale snapshots

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): bump catalog generation on OAuth login success

Every other optimistic provider mutation claims a new generation; the
OAuth success path didn't, so a catalog load or resync still in flight
could arrive late and overwrite the just-connected state.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): resync catalog after OAuth login instead of bare generation bump

The resync claims a new generation (discarding any stale in-flight
response) and its own fetch covers both the new OAuth connection and any
provider saved moments earlier, matching the post-save path.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint (#13626)

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint

Restoring a checkpoint runs git reset --hard, which moves the current
branch pointer. If commits were made after the checkpoint (by the user
or by the agent), the reset silently knocked them off the branch,
leaving them reachable only through the reflog.

Guard the reset: if HEAD no longer matches the commit the checkpoint
was created on, throw a descriptive error (including how many commits
would be dropped) instead of destroying history. Chat-only restore is
unaffected, and users who really want to discard the commits can reset
the branch manually first.

Fixes #13550

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): close the guard-to-reset race with an atomic ref update

The moved-HEAD guard read HEAD, ran further git commands, then reset
unconditionally, so a commit landing in that window could still be
knocked off the branch. Replace the reset's branch move with git's
native compare-and-swap (git update-ref HEAD <new> <old>), which fails
if HEAD no longer points at the verified commit, and follow with a bare
reset --hard to sync the index and worktree to the already-moved HEAD.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: hide history cost estimates for subscription-billed tasks (#13562)

* fix(vscode): hide history cost estimates for subscription-billed tasks

The task-header fix for subscription providers cannot reach history:
history rows render the stored totalCost (an API-rate estimate) and do
not know which provider ran the task, so the history page printed
$X.XXXX on every row and the recent-task chips in an empty chat view
rendered a $ chip even for subscription-billed tasks.

The SDK session records already persist the provider — the CLI's
history view uses it for exactly this — but the VS Code mappers dropped
it. Map it through both transports (HistoryItem.apiProvider for the
state-pushed taskHistory, TaskItem.api_provider for getTaskHistory) and
suppress the dollar figure per row when that provider's
usageCostDisplay is not "show", via a new useUsageCostVisibility
predicate shared by both surfaces.

Rows without a recorded provider (tasks predating the field, legacy
imports) keep showing the stored value — there is nothing to key
suppression on.

* test(vscode): e2e-verify history cost suppression in real VS Code

Seeds SDK session records (one openai-codex subscription task, one
anthropic usage-billed task) into the isolated CLINE_DIR before the
webview loads, then asserts in a real VS Code instance that both the
recent-task chips and the full history page render the dollar figure
only for the usage-billed task. Covers the two boundaries the unit
tests stub: on-disk records reaching getTaskHistory with provider
populated, and the provider listings delivering the subscription mark
to the webview.

* Fix scheduled tasks disappearing after desktop app updates (#13627)

* Fix hub-managed schedules being wiped by cron reconciliation on hub restart

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Require the virtual hub/schedules path when exempting specs from removal reconciliation

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Treat recorded source mtime as proof a spec is file-backed, closing the hub/schedules spoof gap

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(shared): discover global rules at ~/Cline/Rules (#13614)

The VS Code Rules tab resolves the Documents folder via
'xdg-user-dir DOCUMENTS', which prints bare $HOME when no user-dirs
config exists (WSL/headless installs), so it reads and writes global
rules at ~/Cline/Rules. The SDK's rule search paths only covered
~/Documents/Cline/Rules, so those rules never reached the system prompt.
Add the missing path to the search list.

Fixes #13542

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat: add searchable session history (#13420)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* Fix CLI crash when a remote MCP server is offline but enabled (#13639)

Remote (SSE/streamable HTTP) MCP connects run on the session.create
critical path, which the hub caps at 30s. Without a connect budget an
unreachable server spent the full 60s default request timeout (with the
SSE transport stuck in a reconnect loop), stalling session.create past
the hub deadline and tearing the whole session down - the interactive
TUI exited and one-shot runs failed. Stdio servers already have a
bounded initialize budget for exactly this reason; give URL clients the
same treatment with a 10s default connect budget that an explicit
timeout overrides in either direction.

Fixes #13597

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(vscode): prevent E2E worker teardown hangs (#13644)

* test(vscode): capture external URLs in E2E runs

* docs(test): clarify browser capture rationale

* Add a GitHub integration step to the onboarding (#13225)

* Add feature flags to the app

* React to account updates

* Address comments

* Add a GitHub integration step to the onboarding

* validate domain and fix errors on auth

* Hide the step behind a feature flag

* update version

---------

Co-authored-by: John Choi <john.choi@cline.bot>

* fix(ci): stop e2e worker teardown timeouts and deflake hub daemon e2e on Windows (#13646)

* fix(e2e): stop VS Code e2e worker teardown from timing out

The ext-vscode-test-e2e job has been failing on main with 'Worker teardown
timeout of 60000ms exceeded' even though every test passes. Playwright only
reports an Electron app as closed once the process exits AND every holder of
its stdio pipes is gone (ChildProcess 'close' waits on the extra fd3/fd4
pipes Playwright creates for Electron). Any VS Code descendant that outlives
the main process (chrome_crashpad_handler, GLib's 'dconf watch' helper,
xdg-open browser handlers, VS Code 1.135's agent host CLI subprocess that
logs 'unable to kill the process') keeps those pipes open, so app.close()
never resolves and the worker teardown hangs on it until its 60s timeout
fails the job.

Harness fixes, each removing one source of that wedge:

- closeAppForTeardown now SIGKILLs the whole process group (taskkill /T on
  Windows) when app.close() times out, instead of only the main pid — and
  does so even when the main process already exited, which is exactly the
  wedged state. Playwright launches Electron detached, so pid == pgid.
- Launch VS Code with --disable-crash-reporter so no crashpad handler
  outlives the app holding the harness pipes.
- Seed the fresh user-data-dir with chat.disableAIFeatures: true so VS
  Code's own AI features (rolled out via server-side experiments, so CI
  breaks without any repo change) never start their agent host process.
- Drop the page.close() teardown: closing VS Code's last window quits the
  whole app, and ElectronApplication.close() on an already-exited app
  deadlocks; the app fixture's app.close() closes windows itself while the
  app is alive.
- Codex sign-in no longer opens a real external browser under E2E_TEST; the
  codex-oauth test drives the OAuth callback itself, and the browser was an
  orphaned process holding the harness pipes on the runner.

* fix(core): deflake hub daemon e2e tests on Windows runners

sdk-test on windows-latest fails intermittently in the hub daemon e2e
files:

- shutdown.e2e.test.ts dies with a bare 'Error: socket hang up'. That
  message is the ws handshake (http.ClientRequest) failing, not the
  /shutdown fetch (an undici failure prints 'TypeError: fetch failed'):
  a freshly spawned bun daemon on a loaded 2-core Windows runner
  occasionally drops its first accepted connection before writing the
  upgrade response. Real hub clients reconnect with backoff, and the test
  asserts shutdown behavior rather than first-connection reliability, so
  openAuthenticatedSocket now retries transient handshake failures within
  a 15s budget.
- singleton.e2e.test.ts times out waiting for daemon discovery: it still
  used the 10s hang guard that 0cfc90158 already raised to 30s in
  shutdown.e2e.test.ts for the same reason. Use the same 30s guard.
- Raise the e2e testTimeout to 60s so a test that legitimately spawns two
  daemons back to back can survive slow-runner startups instead of the
  discovery hang guard being cut off by the test timeout.

* feat(desktop): render tool output images as attachments (#13643)

* fix(desktop): render tool output images as attachments

Add support for displaying media returned by tool calls (e.g. screenshots)
as rendered images with expand-to-fullscreen capability instead of raw
base64 text. Introduces an `ImageCarousel` component for navigating
multiple images, propagates the expand handler to tool message blocks,
and extracts/validates output media in tool summaries.

* test: cover multi-image and canonical media extraction in tool output (#13645)

extractOutputMedia and the desktop tool-message rendering path were only
ever exercised with exactly one distinct valid image, and
canonicalInlineMedia (MCP-style type: "media" blocks for audio/video/file)
had zero coverage. Add tests for: multiple distinct images in one tool
result (parser + desktop carousel navigation), inline audio via the
mime_type key spelling, canonical video/file media blocks, and rejection
of an invalid canonical image block.

---------

Co-authored-by: Harrison <harrison@cline.bot>

* chore(desktop): release v0.0.20

* feat(sdk): add discovery boundary ahead of Agent Plugins support (#13017)

* ENG-2490: Propagate session aborts to teammates (#13647)

* fix(core): propagate session abort to teammates

* fix(core): persist aborted teammate tasks as cancelled

* fix(core): settle teammate work on session abort

* fix(core): isolate replacement runs from stale aborts

* refactor(core): narrow teammate task status metadata

---------

Co-authored-by: abeatrix <beatrix@cline.bot>

* fix(llms): use AI SDK 7 Langfuse telemetry (#13651)

* fix(llms): use AI SDK 7 Langfuse telemetry

* test(llms): cover Langfuse runtime context

* chore(llms): built-in model list update 1787907289186 (#13663)

* chore(llms): built-in model list update 1787907289186

Result of `bun run build:models`.
Includes updated model list and fixed formatting issues across codebase.

* test(llms): update GLM reasoning toggle expectation

* test: cover session search fallback on hub timeout and rejection (#13642)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

* test: cover sidecar search fallback on hub timeout and rejection

The existing search_sessions tests only exercised the index-hit and
empty-index-fallback paths with an immediately-resolved hub reply.
Add coverage for the two other realistic Hub-connection failure
modes the fallback is meant to tolerate: the hub call rejecting, and
the hub call hanging past the 750ms withSearchDeadline race.

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* fix(core): refresh Cline models from live catalog (#13670)

* feat(ui): share attachment drop zone (#13672)

* feat(ui): share attachment drop zone

* fix(ui): cancel disabled attachment drops

* chore(ui): simplify drop zone surface

* chore(ui): release v0.2.0-next.8

* Chore/bump undici mermaid (#13675)

* chore(deps): bump mermaid to 11.16.1 and raise undici floor to 7.29.0

* chore(deps): patch js-yaml and body-parser in the npm-managed subprojects

* fix(llms): make Langfuse tracer detection survive minified release builds (#13680)

* fix(llms): recognize direct tracer providers

* fix(llms): make Langfuse tracer detection survive minified release builds

Release binaries are compiled with minify enabled, which renames classes,
so initializeLangfuseTelemetry's constructor-name guard never matched
"ProxyTracerProvider" and silently returned readiness=false in every
production build (hub log: "creating span processor" followed by
"initialized readiness=false" with no branch message in between). Dev runs
execute unminified source, which is why the same env vars worked there.

Replace every constructor-name comparison with checks that survive
minification: detect the proxy structurally via getDelegate, distinguish a
recording provider from the no-op fallback by its lifecycle methods, and
confirm our NodeTracerProvider registration by object identity. When a
foreign provider already owns the global slot, attach the Langfuse span
processor to it when it accepts processors, and otherwise shut down the
orphaned provider and report the rejection instead of bailing silently.

Verified by bundling the module with Bun minify:true against the real
OpenTelemetry packages: the previous code reproduces readiness=false
(provider class name mangles to "H2"), the new code initializes with
readiness=true.

* fix(vscode): prevent hook spawn failures from crashing the core process (#13422)

* fix(vscode): prevent hook spawn failures from crashing the core process

A hook child-process spawn failure emitted "error" on HookProcess with no
listener registered, which Node's EventEmitter turns into an uncaught
exception - killing the entire cline-core process instead of failing the
one hook open. Guard the emit behind listenerCount so the rejection (which
StdioHookRunner handles) is the only propagation path.

The trigger was a workspace root that no longer exists on disk passed as
the spawn cwd: Node reports a nonexistent cwd as a misleading ENOENT on
the launcher binary ("spawn /bin/sh ENOENT"). Validate cwd existence in
HookProcess right before spawning - falling back to no explicit cwd with
a warning that names the missing directory - and when a spawn still fails
ENOENT because the directory vanished in between, name it in the error
message instead of blaming the shell.

* fix(vscode): fail hooks with a missing working directory instead of relocating them

Running a hook whose assigned cwd no longer exists from the host
process's own working directory would let its relative paths read and
write an unrelated location (e.g. the IDE install directory). Reject
before spawning, with an error naming the missing directory; the runner
reports the hook as failed and the task continues. Also carry pre-spawn
failure messages into HookExecutionError details so the cause is not
reduced to a bare "exited with code 1".

* fix(vscode): thread task id into hook runner creation so execution telemetry fires (#13547)

The SDK hooks adapter created every hook runner without a task id, and
StdioHookRunner gates all captureHookExecution calls on one being set —
so the next variant emitted zero hooks.execution events while discovery
telemetry fired normally. Pass the task id (and tool name for the tool
hooks) at all five factory.create call sites, and pin the threading
with a regression test.

* Desktop marketplace redesign: two-pane explorer with full catalog metadata (#13653)

* feat(desktop): add marketplace design exploration prototypes (storefront, explorer, registry)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): render catalog icon tiles without percentage padding

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): drop placeholder icon tiles from explorer marketplace direction

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): make explorer the marketplace view, drop design exploration harness

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): add category tag filters to marketplace explorer

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): collapse marketplace category pills behind a more toggle

* feat(desktop): remove maturity badges and CLI install section from marketplace

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* fix(core): propagate parent aborts to delegated subagents (#13677)

* fix(core): propagate parent aborts to delegated subagents

* docs(core): narrow delegated abort guarantees

* fix(core): release delegated sessions after execution

* fix(core): scope abort listeners to active runs

* fix(core): inherit parent runtime pid for subagents

* fix(desktop): keep Stop available for running child agents (#13678)

* fix(desktop): keep Stop available for running child agents

* fix(desktop): reconcile aborted tool activity

* fix(desktop): guard abort and agent polling races

* fix(desktop): preserve authoritative abort status

* fix(desktop): track queue-verified completion

* test(desktop): trim duplicate abort coverage

* fix(desktop): settle delayed queue verification

* fix: sanitize stored API keys and make provider credential rejections actionable (#13549)

* fix(vscode): sanitize pasted provider API keys at the settings write boundary

Clipboards smuggle control and invisible formatting characters (newlines,
zero-width spaces, BOM) into pasted API keys. The masked key field hides
the corruption and providers reject the key with a 401 indistinguishable
from a genuinely wrong key. Strip those characters and surrounding
whitespace once in the provider config store write path, so both backing
stores (legacy state secrets and providers.json) receive the clean value.
A whitespace-only value now clears the key.

* feat(llms,vscode): classify provider 401/403 as auth errors and surface actionable guidance

Add an "auth" ProviderErrorClass, assigned when the HTTP layer reports
401/403 — status-only on purpose, since provider bodies can quote words
like "unauthorized" without the request being an auth failure. The class
rides the existing errorClass plumbing (finish -> run-failed ->
AgentErrorEvent), so every host receives it with no new wiring.

In the VS Code chat surface, rewrite classified credential rejections
from BYOK providers into actionable text pointing at the API key
configuration, keeping the provider's raw body as a diagnostic tail.
Raw bodies alone are dead ends: Mistral, for example, answers an
identical {"detail":"Invalid API Key"} for a wrong, empty, or
wrong-scope key. Cline-account providers keep the JSON path so the
webview still renders their auth failures as a sign-in card.

* Fix ask-question option text not wrapping (#13718)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.21

* fix(core): stop an empty capability list from stripping image input (#13583)

`modelHasCapability` documents a missing or empty capability list as
carrying no signal, so each gate declares its own default. Two readers
bypassed it and read `capabilities` directly, where an empty list is not
nullish but `[].includes(x)` is false:

- the session runtime's `modelSupportsImages` metadata used
  `capabilities?.includes("images") ?? true`, so the intended fail-open
  never fired for an empty list and the file-read tool silently dropped
  every image from the request;
- `toProviderModel` projected an empty list onto `false`, telling pickers
  a model definitively lacks vision, attachments, and reasoning when
  nothing had been declared.

Both now route through the shared helpers, which state their unspecified
default explicitly: `modelSupportsImageInput` fails open for a capability
gate, and `declaredCapability` preserves `undefined` for `ProviderModel`'s
tri-state booleans. A populated list stays authoritative in both.

A thinking config now short-circuits `supportsReasoning` instead of being
OR-ed with the capability read, so its absence no longer collapses the
tri-state to `false`.

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* fix(llms): translate gateway capabilities in one place (#13584)

* fix(core): stop an empty capability list from stripping image input

`modelHasCapability` documents a missing or empty capability list as
carrying no signal, so each gate declares its own default. Two readers
bypassed it and read `capabilities` directly, where an empty list is not
nullish but `[].includes(x)` is false:

- the session runtime's `modelSupportsImages` metadata used
  `capabilities?.includes("images") ?? true`, so the intended fail-open
  never fired for an empty list and the file-read tool silently dropped
  every image from the request;
- `toProviderModel` projected an empty list onto `false`, telling pickers
  a model definitively lacks vision, attachments, and reasoning when
  nothing had been declared.

Both now route through the shared helpers, which state their unspecified
default explicitly: `modelSupportsImageInput` fails open for a capability
gate, and `declaredCapability` preserves `undefined` for `ProviderModel`'s
tri-state booleans. A populated list stays authoritative in both.

A thinking config now short-circuits `supportsReasoning` instead of being
OR-ed with the capability read, so its absence no longer collapses the
tri-state to `false`.

* fix(llms): translate gateway capabilities in one place

Three producers built gateway model definitions from catalog `ModelInfo`,
and each carried its own hand-written `switch` over the capability list.
Nothing tied them together, so they drifted:

- builtin providers always emitted a capability list, so a model whose
  catalog entry declares no capabilities became `["text"]` where the other
  producers emitted `undefined`. `modelSupportsToolCalling` fails open only
  for an absent or empty list, so that list read as an authoritative denial
  and stripped every tool definition from requests to the affected language
  models (dify, sapaicore, opencode, and the Codex CLI);
- the OpenAI-compatible path mapped an `audio` capability that
  `ModelCapabilitySchema` does not define, while the other two dropped it;
- the pass-through capabilities (`streaming`, `files`, `temperature`, ...)
  were enumerated explicitly in one, folded into `default:` in another,
  and ignored in the third.

One exported `toGatewayModelCapabilities` now serves every producer. It is
built on a `Record<ModelCapability, GatewayModelCapability | null>` rather
than a `switch`, so extending `ModelCapabilitySchema` without deciding the
new capability's mapping fails to compile instead of silently falling
through to a default.

The conformance tests walk the capability state space taken from
`ModelCapabilitySchema` itself and assert the real producers agree with the
translator, so a future producer that maps capabilities on its own fails
even when the translator's own unit tests still pass.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>

* fix(cli): keep markdown streaming prop stable to stop settle flash (#13719)

Flipping the <markdown> streaming prop from true to false when an
assistant text segment settles makes MarkdownRenderable call
updateBlocks(true), which skips every block-reuse path and destroys and
recreates all block renderables. Until tree-sitter re-highlights them
the whole message renders blank/unhighlighted, which users see as the
text flashing at the end of each response. Keep streaming={true} for
the transcript markdown (opencode's TUI does the same); entry.streaming
still drives the spinner glyph.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Clarify model-facing message when user rejects a tool call (#12673)

* Clarify model-facing message when user rejects a tool call

* Include the rejected tool's name in denial reasons

* Move user-rejected tool reason into @cline/shared

* Route new user-rejection approval paths through shared reason builder

Since the original PR, several new approval surfaces landed on main with
their own terse denial strings (CLI connectors, ACP permissions, Cline Hub
webview, desktop webview, example VS Code extension). Route all of them
through buildUserRejectedToolReason so the model sees a consistent,
non-error rejection message.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add buildUserRejectedToolReason to the @cline/shared integration-test stub

The VS Code integration tests run the tsc-built CJS tree and stub the
ESM-only @cline/shared package in test-setup.js; the stub was missing the
new export, so tool-approval-denial.js threw at module load in CI.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Trim scope back to the minimal rejection-copy fix

Restore the connector deniedReason plumbing, ACP permission strings,
desktop webview reason, example extension reason, and hub server fallback
to their main versions. Those surfaces already attribute the denial to a
user and are outside ENG-2329. Keep the Cline Hub webview change since
that path emits its own rejection string the model sees.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Move rejection guidance suffix into agent runtime per review

* Apply review suggestions: neutral fallback reason and -- separator before rejection suffix

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Default web search on for the desktop app (#13725)

* Default web search on for the desktop app

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make desktop web search default seed best-effort

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* fix(desktop): reconcile experimental sync behavior

* chore(desktop): bump beta to 0.0.22-beta.1

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: JasmineLCY <38378321+JasmineLCY@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Haley Park <haleypark.design@gmail.com>
Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>
Co-authored-by: yzxcj797 <54314860+yzxcj797@users.noreply.github.com>
Co-authored-by: yzxcj797 <yzxcj797@users.noreply.github.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: 𝓜𝓲𝓼𝓼𝓪𝓻𝓲 𝓐𝓱𝓲𝓵 🌿 <143264692+missarii@users.noreply.github.com>
Co-authored-by: Dominic Cooney <dominic.cooney@cline.bot>
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Harrison <harrison@cline.bot>
Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: TheRealSpencer <32678829+TheRealSpencer@users.noreply.github.com>
2026-09-01 15:16:51 -07:00
+14 af66391727 chore(desktop): sync latest main into desktop experimental (#13742)
* fix(vscode): continue the surviving session on resume instead of rebuilding with the original task text (#13175)

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilt the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

The preserved conversation history is the source of truth on resume, so
the fallback prompt now just asks the model to reassess the history and
continue, matching the legacy resume prompt which also never resent the
original task. User-typed text still takes precedence when provided.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): continue the surviving idle session on follow-ups instead of rebuilding

Stopping a turn keeps the session alive, but every idle follow-up (bare
Resume after Stop, and typed follow-ups after a completed turn) tore
that session down and rebuilt it from persisted task history before
sending. Continue the matching idle session in place instead, the same
way the CLI reuses the live session after an abort. Rebuilding from
history now only happens when no live session matches the displayed
task (task opened from history, extension host reload).

A bare resume still needs a prompt to start a turn, so it sends the
neutral [TASK RESUMPTION] prompt (shared with the rebuild fallback and
hidden from the transcript); user-typed content is echoed and sent
as-is. If the send lands while the abort is still settling, the runtime
auto-queues it and drains once the abort completes.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): consolidate follow-up send paths in SdkFollowupCoordinator

Now that idle follow-ups continue the live session in place, the
two-mode sendToActiveSession helper was redundant: its non-queued branch
duplicated continueIdleSession minus the bare-resume prompt. Split it
into a single-purpose queueToActiveSession and fold the idle no-task
send into continueIdleSession, flattening askResponse's decision tree
to: queue onto a running turn, continue a matching live idle session,
rebuild from history, or abandon.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): reuse the existing neutral resumption prompt for bare resumes

Drop the newly invented long resumption wording in favor of the phrase
that already existed as the no-history fallback and that the transcript
hiding logic and test fixtures recognize: '[TASK RESUMPTION] Please
continue where you left off.' The net change to resumeSessionFromTask
against main is now just deleting the branch that resubmitted
historyItem.task as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilds the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

Bare resumes now always use the neutral prompt that already existed as
the no-history fallback; user-typed text still takes precedence. This
matches the legacy resume prompt (responses.taskResumption), which only
ever included user-supplied text as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): hide synthetic prompts from the queued-prompt echo

A send that races a settling abort is auto-queued by the runtime, so a
bare Resume can reach the pending_prompt_submitted echo carrying the
synthetic [TASK RESUMPTION] prompt. Echoing it leaked model-facing text
as a visible user bubble and shifted the visible-user-message ordinals
that edit/regenerate mapping relies on. Filter synthetic prompts with
isSyntheticUserPrompt, keeping user attachments visible (matching
isSyntheticSdkUserMessage semantics).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): preserve LiteLLM input token limits (#13293)

* fix(vscode): preserve LiteLLM input token limits

* fix(vscode): prefer live LiteLLM model metadata

* fix(vscode): generalize private catalog metadata

* test(vscode): preserve llms exports in vscode lm mock

* fix(vscode): point provider signup URLs at their API key pages (#13337)

* fix(vscode): point Mistral signup URL at the general API keys console

The Mistral provider's signup link led to the Codestral console, which
issues Codestral-scoped keys that fail with 401 on api.mistral.ai — the
endpoint the provider actually calls. Point it at the general API keys
page instead.

Fixes #13288

* fix(vscode): deep-link DeepSeek and Fireworks signup URLs to their API key pages

Both pointed at marketing homepages; link straight to the key-creation
pages instead, matching the rest of the registry and the desktop app's
provider-key-urls map.

* fix(ci): always build the legacy bundle from the legacy-extension branch (#13349)

The combined-VSIX workflow took legacy-ref as a free-form dispatch input
with no publish-time validation (next-ref has one: publish requires main).
Any typed ref — a PR merge ref, an unprotected branch — would be built
into the published VSIX by the environment-less build job, and the publish
environment approver only ever sees an opaque prebuilt artifact, so the
approval protected the marketplace PAT but not the shipped bytes.

Remove the input entirely and hardcode the protected legacy-extension
branch, which makes that branch's protection rules load-bearing for
releases. The tested-sha pinning between test-legacy and build is
unchanged. publish-extension skill dispatch command updated to match.

* fix(ci): lock the legacy publish workflow to the legacy-extension branch (#13350)

The branch dispatch input was a free-form string with no validation. Both
jobs checked it out and ran full npm lifecycle scripts from it: the publish
job next to VSCE_PAT/OVSX_PAT (and npm run publish:marketplace executes a
script from that same ref with the PATs in env), and the test job with NO
environment approval at all while inheriting the workflow-level
contents/packages/checks/pull-requests write grants. A dispatch pointing at
e.g. refs/pull/N/head would run outside-contributor code with the
marketplace keys behind one approval, or with a repo-write token behind
none.

Remove the input and hardcode the protected legacy-extension branch, drop
the workflow-level permissions to contents: read, and elevate only the
publish job to contents: write (tag push + GitHub release). The branch
input's default was legacy-extension, so normal publishes are unchanged.
publish-extension skill dispatch command updated to match.

* fix(vscode): SDK remote-config parity — refresh coordination, session gating, and fail-closed opt-out (#13226)

* feat(desktop): native notifications (#13166)

* feat(desktop): native notifications

* macos target

* fix(desktop): isolate macOS dev app identity

* fix(desktop): address notification review feedback

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched (#13310)

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched

Toggling an auto-approve setting while a task is open writes
autoApprovalSettings into the StateManager's task-settings overlay
(updateAutoApprovalSettings -> setTaskSettings). The SDK controller never
cleared that overlay on clearTask/showTaskWithId (the legacy controller
did), so after New Task the stale overlay kept shadowing global settings
in getGlobalSettingsKey(): toggle RPCs were accepted into global state,
but every posted state still carried the overlay's old version, which the
webview rejects as not newer - the auto-approve checkboxes froze forever.

Restore legacy parity in SdkTaskControlCoordinator: drop the overlay
(persisting pending writes first) in clearTask() and before installing a
different task's proxy in showTaskWithId().

Fixes #13260

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* changeset

* test(vscode): add end-to-end regression test for auto-approve freeze after New Task

Wires the real StateManager, the real updateAutoApprovalSettings handler,
and the real SdkTaskControlCoordinator.clearTask() together with the
webview's version gate modeled on ExtensionStateContext, pinning the
end-to-end invariant behind #13260: checkbox toggles must keep reaching
the webview after a mid-task toggle followed by New Task. Verified the
test fails when the clearTaskSettings() call is removed from clearTask().

* fix implicit any in regression test

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): show provider web-search support under the settings toggle (#13328)

* feat(desktop): show provider web-search support under the settings toggle

The global Web search toggle silently does nothing unless the session's
provider offers native web search, which made the setting read as if it
worked with any provider. The desktop General settings row now explains
that only providers with built-in web search honor it, and shows a live
status line: which connected providers are ready to use it (no extra
setup needed), or an amber warning with a link to the Models section
when none of them support it.

Support is resolved in the webview via a new providerOffersModelTool
helper in @cline/llms (browser export), sharing the same builtin-manifest
source of truth as the runtime's supportsModelTool attachment check.

* fix(desktop): address review — refetch web-search status on catalog invalidation, clarify per-model support

Greptile P2: the one-time catalog fetch could race an in-flight provider
save and show stale status; the row now refetches when the provider
catalog cache is invalidated (fired after saves complete).

Greptile P1: the ready line implied every model on the provider works;
Vertex excludes Claude routes, so the copy now scopes the promise to
models that support it.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* feat(ui/desktop): collapse finished runs into a work summary and remove hover-state dead space (#13315)

* feat(ui): add WorkActivity collapsed-run summary and float message actions as a pill

WorkActivity/WorkActivityTrigger/WorkActivityContent fold a finished agent
run's working rows (tool calls, thinking traces, narration) behind a single
"Worked for 4m 12s · 14 tool calls" disclosure built on the shared animated
disclosure primitives, with formatWorkActivityLabel/formatWorkDuration
exported for consumers.

Message hover actions no longer rely on the transcript reserving blank space
below each message: the action row is now a self-backed pill (border,
blurred background, shadow) that floats over whatever follows, so
conversations can pack rows tightly without hover chrome colliding with the
next message.

* feat(desktop): collapse finished runs into a work summary and tighten chat spacing

collapseCompletedWork post-processes the grouped transcript: once a run ends
on assistant text with no further tool calls, its working rows fold into one
expandable WorkActivity row while the final answer stays visible. Runs are
delimited by user messages; the trailing run only collapses when the session
has stopped running and actually produced an answer, so live streams and
cancelled/failed tails keep their rows. Assistant messages carrying images
or media are treated as deliverables and never collapse.

The conversation list gap drops from gap-8 to gap-4 now that hover actions
are self-backed pills that need no reserved space, and user messages add
their own top margin so turn boundaries stay visually distinct.

* refactor(ui/desktop): work summary label wording, flat expansion, stable in-run rhythm

Feedback round on #13315:

- Label reads "Worked for 4m 12s and made 14 tool calls" instead of joining
  with a dot; without a duration it falls back to "Made N tool calls".
- Expanded work rows render at transcript level — no rail or extra indent —
  since tool rows and thinking traces already carry their own nesting when
  expanded. The work content keeps the tight working-row rhythm.
- Live working rows (thinking traces + tool calls) now group into a 'run'
  render item with the same tight 0.25rem rhythm, so there is no oversized
  gap under a "Thought for Ns" row and every row keeps its exact position
  when the finished run folds into the work summary. A trailing
  answer-in-progress stays outside the group at transcript level, and pure
  prose spans keep normal spacing.
- The transient "Thinking..." indicator moves inside the transcript column
  and mirrors a trigger row's geometry, so the first real row replaces it in
  place with no jump.

* style(ui/desktop): hover-pill metrics, right-pointing work chevron, scroll and spacing fixes

Another feedback round on #13315:

- Hover action pill: +2px internal padding, a trailing inset after the
  timestamp (it sat flush against the pill border), and more clearance
  between the message content and the pill (2px -> 6px; the hover bridge
  grows to match).
- The work summary chevron points right while collapsed and continues
  counterclockwise to point up when expanded.
- Conversation bottom padding drops pb-20 -> pb-8: the composer sits below
  the scroller, so the padding only needs to clear a pinned action pill.
- Sending a message scrolls back to the bottom even if the reader had
  scrolled up (new AutoScrollOnSend on the user-message count, which ignores
  optimistic-bubble re-keying; @cline/ui now exports useConversation for
  this).
- An assistant answer directly under its run's working rows pulls itself
  0.5rem closer than the full transcript gap.

* style(desktop): leave a visible gap between a pinned action pill and the composer

pb-8 exactly matched the pill's ~40px footprint, so the last row's hover
actions sat flush against the composer top; pb-12 restores ~8px of daylight.

* style(desktop): widen the gap between the pinned action pill and the composer to ~24px

pb-12 left only ~8px of daylight under the pill; pb-16 reads comfortable
without reverting to pb-20's dead space.

* fix(desktop): keep the thinking indicator at the working-row offset mid-run

The indicator matched a trigger row's geometry but sat a full transcript gap
(1rem) below the last working row, while the tool/thinking row replacing it
joins the tight run group at 0.25rem — a visible upward jump. When the last
transcript item is working rows (or streamed assistant output), the
indicator now pulls up to the same tight offset; only at the start of a run,
under the user message, does it keep the normal gap.

* style(ui): calm the hover actions surface per team feedback

Borderless rectangle instead of the bordered pill: radius drops to
var(--radius), the side padding goes entirely (the icon buttons carry their
own hit areas), and the vertical padding halves. Blurred background and
shadow stay so it remains legible over following content.

* feat(ui/desktop): full-band hover reveal and iOS-style disclosure easing

The hover actions only appeared while the pointer was inside the message
box itself. The invisible bridge under each message now spans the full
height of the band the floating actions occupy (full row width), so
hovering anywhere in that strip reveals them. Sibling row types
(.cline-chat-tool, .cline-chat-work, and the desktop's run/tool groups)
become position: relative so they paint above the bridge — their own
content keeps its hover and clicks, and the bridge only wins in the band's
genuinely empty space.

All expandable rows (work summary, tool panels, thinking) open and close on
a 240ms symmetric ease-in-out cubic-bezier instead of the 60ms snap, with
chevron rotation on the same curve. Reduced-motion still disables both.

* revert(ui/desktop): drop the full-band hover reveal; quicken disclosure easing to 180ms

The full-band hover bridge (and the position: relative changes that made it
safe) is reverted per feedback — back to the narrow bridge that only spans
the gap under the message. The iOS-style ease-in-out on disclosures stays
but speeds up from 240ms to 180ms.

* fix(ui): recover live tool diffs that mount as a blank pierre skeleton

Live-streamed edit rows could show an empty diff for the whole run, with the
diff only appearing after the collapsed work row was expanded (fresh mount).
Root cause, confirmed by driving a live session and inspecting the element:
React StrictMode double-invokes @pierre/diffs' ref callback; the first
instance's async highlight work aborts on its immediate cleanup, and the
second instance adopts the abandoned half-rendered shadow tree as if it were
complete prerendered output — zero height, no code, no theme stylesheet,
permanently. A rendered diff always carries style[data-theme-css] in its
shadow root, so ToolFileDiff now checks for it shortly after mount and
remounts FileDiff (bounded attempts) when missing; the fresh host element
takes the normal render path and recovers within ~400ms. Verified live: the
diff now renders during the run.

* fix(desktop): keep interrupted runs expanded even with partial trailing text

The trailing-run collapse gated on 'ended with assistant text', which
misread a Stop that landed mid-answer as a finished run and folded the tool
calls the user wants to inspect. The gate is now the terminal status itself:
only completed (or restored-idle) sessions collapse the trailing run;
cancelled/failed/error tails keep their rows regardless of partial text.
(Greptile P1 on #13315 — matches the PR's stated rule.)

* feat(ui): share the markdown pipeline, chat polish, and ThinkingBlock across products (#13323)

* feat(ui): share the markdown pipeline, chat polish CSS, and ThinkingBlock

The desktop app and the cloud dashboard both consume @cline/ui yet rendered
assistant output differently, because Markdown policy and the thinking-trace
row lived app-side. This moves the shareable parts into the package:

- components/markdown (new export): the lazy Shiki code highlighter (GitHub
  light/dark, pinned language set) and agentMarkdownControls — the standard
  Streamdown configuration. streamdown/shiki/@shikijs/* become optional peer
  dependencies, mirroring @pierre/diffs.
- components/markdown.css: the desktop's chat polish moves in — chat-scale
  headings, outside list markers, single quiet code blocks with a
  hover-revealed copy control, table cards. Kept unlayered so it beats
  Streamdown's layered Tailwind utilities without !important.
- ThinkingBlock + formatThoughtLabel in agent-chat: the standard thinking
  row (brain icon, Thinking/Thought-for-Ns label, streaming shimmer, rail
  presentation, capped scrollable body). The shimmer and the
  reasoning-hover-suppression rule move into agent-chat.css; triggers gain
  the color transition the desktop applied locally.

Version bumps to 0.2.0-next.5 for the dashboard to pick up.

* refactor(desktop): consume shared markdown and thinking primitives from @cline/ui

The local Shiki highlighter, Streamdown controls, chat markdown polish CSS,
streaming-title shimmer, and reasoning hover-suppression rule are deleted in
favor of the @cline/ui versions (the highlighter test moves to the package's
suite). ReasoningBlock becomes a thin wrapper that hands MemoizedMarkdown to
the shared ThinkingBlock, and formatThoughtLabel re-exports from the package
so grouping code and tests keep their import path.

globals.css now imports @cline/ui/components/markdown.css (unlayered, so the
polish keeps beating Streamdown's layered utilities); the app keeps only what
is genuinely app-specific: link/image policy in markdown.tsx, selectability
rules, accent palettes, and the view-enter transition.

* style(ui/desktop): make thinking-trace prose legible

Thinking body text rendered too faint: plain muted-foreground plus the
desktop's font-thin weight. The shared thinking content now leans 75% of the
way back toward the body text color (still slightly de-emphasized), and the
desktop drops the thin font weight.

* ci(ui-publish): build @cline/shared before ui typecheck (#13354)

@cline/ui's generated-media imports @cline/shared/browser, which resolves to
shared's dist output. The build-shared step sat after typecheck/test/build,
so the first ui-publish dispatch since #13025 failed at Typecheck UI with
TS2307. Move the step to right after install.

* fix: run_commands object form without args routes through the shell instead of failing with ENOENT (#13336)

* fix: run_commands object form without args routes through the shell

The structured { command, args? } form of run_commands was always spawned
directly with shell: false. When a model emitted a full command line in
command with no args (e.g. { command: "echo hello" }), spawn failed with
ENOENT for any command containing a space, breaking command execution for
the whole session.

Direct exec now only applies when a non-empty args list is provided; the
object form without args is routed through getShellInvocation like the
string form. Schema descriptions are tightened so models put arguments in
args instead of embedding them in command.

Fixes #13279

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: trim structured-command schema descriptions

The union schema is only used for lenient validation of input the model
already sent; its descriptions never reach a model prompt. Keep them
short instead of restating executor behavior.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: simplify direct-exec comment in shell executor

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert: keep original structured-command schema description

The description never reaches a model prompt and the executor now handles
both shapes, so the wording change was cosmetic noise.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: gate direct exec on args key presence, not array length

Review feedback: an explicit empty args array is intentionally structured
input and stays direct exec; only an object with no args key is treated
as a full shell command line. Matches the key-presence rule already used
by the VS Code host's formatCommandForTerminal. Also replaces the
empty-args shell test (which was PowerShell-incompatible) with a test
pinning the direct-exec contract.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: normalize Gemini custom base URLs for legacy host-root values (#13329)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs: add GLM-5.3 to ClinePass models and reference pricing (#13357)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): stream run command output (#13179)

* feat(desktop): stream run command output

* fix(sdk): clean up detached command logs

* fix(sdk): reap detached logs after hub restarts

* fix(sdk): preserve live detached command logs

* fix(desktop): harden live command progress

* fix(sdk): recover detached logs for local hosts

* fix(desktop): reconcile command output tool rows

* fix(sdk): retain logs for surviving commands

* fix(core): prevent PID reuse from retaining detached logs

* fix(core): preserve detached logs on probe failures

* fix(core): retain detached logs during probe outages

* fix(desktop): resolve leftover merge conflict in messages projection test

Combine both sides of the assertion: main's incremented per-block
createdAt projection and this branch's toolCallId/hookEventName meta.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): make TUI dialog colors follow theme changes live (#13355)

* fix(cli): make TUI dialog colors follow theme changes live

Dialog content previously read the static palette constant, so open
dialogs (including the theme picker itself) kept the default dark-blue
accents while scrolling through theme previews. Add getDialogPalette /
useDialogPalette, which resolve dialog colors from the active theme's
dialog accents and re-render on every theme change, and migrate all
dialog-rendered components to it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(cli): derive dialog panel background from the active theme

Dark themes now lift their own background one OKLAB step for the dialog
surface, so panels keep the theme's hue instead of the library's fixed
#262626. DialogThemeSync pushes the surface into the dialog container
for new dialogs and repaints open panels, so the surface also follows
live theme previews. Light themes keep the neutral dark panel to match
the dark accent fallback and the light-on-dark dialog text.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: skill slash commands load via the skills tool instead of expanding into the user message (#13327)

* fix(desktop): show typed slash command instead of expanded skill markdown

The sidecar expands /skill and /workflow tokens into their instructions
before dispatching, so the runtime's persisted transcript only contains the
expanded text. After a turn (and when reopening a session) the webview
re-hydrates from that history and rendered the whole SKILL.md body as the
user's message; queue events echoing the expanded prompt could also add a
second user bubble, and fresh sessions were titled with the markdown's first
line. The CLI never shows this because its TUI keeps the typed text in its
own transcript and only sends the expanded prompt to the model.

Mirror that separation inside the desktop sidecar's display boundaries:

- history projection (readSessionMessages) inverts user text that starts
  with a configured command's instructions back to '/name remainder',
  which also repairs sessions recorded before this fix
- queue snapshots and chat_queued_prompt_start events echo the typed
  prompt recorded at expansion time, so the webview's optimistic-bubble
  re-key matches again
- an untitled session sent an expanded prompt gets titled from the typed
  command instead of the instructions' first line

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't overwrite a mid-turn rename with the typed-command title

The untitled check ran before dispatch, so renaming a fresh slash-command
session while its first turn was running got clobbered by the post-turn
typed-command title. Re-check at write time and only replace a missing title
or the one the runtime auto-derived from the expanded prompt.

Also documents the inherent prefix-inversion ambiguity flagged in review:
text hand-typed with a command's exact instructions persists byte-identically
to that command's expansion, so stored history alone cannot distinguish them.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop expanding skill commands; let the skills tool load them

Pasting the skill body into the prompt is why the transcript could ever show
it: the desktop webview re-hydrates from the runtime's persisted history, so
whatever the sidecar splices into the user message renders as if the user
typed it. The runtime already registers the skills tool, whose description
requires the model to invoke it whenever the user references a slash command
— so send the typed /skill text through and let the tool deliver the
instructions as a tool result (previously they arrived twice: pasted and via
the tool). The persisted user message, session title, and queue entries are
then simply the typed command, which deletes the typed-prompt registry, the
queue event/snapshot rewriting, and the title machinery from the previous
approach.

Workflows are not served by the skills tool and keep textual expansion, so
the read-time display inverter stays: it collapses expanded workflow prompts
— and skill prompts persisted before this change — back to the typed
/command in the history projection.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): option to keep skill slash commands typed for the skills tool

resolveRuntimeSlashCommandFromWatcher (and the hub snapshot proxy) accept
expandSkillCommands: hosts whose sessions register the skills tool pass
false so the typed /skill goes through and the model loads the instructions
as a tool result, keeping the persisted transcript as what the user typed.
Workflows always expand — the tool does not serve them. isSkillsToolAvailable
exposes the catalog check hosts use to decide (yolo preset and the skills
tool toggle leave textual expansion as the only delivery path).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): skill slash commands load via the skills tool instead of expanding

The TUI user-command wrap and buildUserInputMessage now keep a typed /skill
as-is when the session's mode/toggles register the skills tool, matching the
desktop app; workflows keep expanding, and yolo (zen) keeps expanding skills
because its preset has no skills tool. This also fixes CLI resume/history
surfaces showing the skill body: the persisted user message is now the typed
command.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): keep configured skill slash commands typed for the skills tool

expandSlashCommands no longer splices a configured skill's instructions into
the model text; the SDK session's skills tool delivers them as a tool result
(previously they arrived twice). Builtin pseudo-skills like /deep-planning
are not served by that tool and keep expanding, as do workflows.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): use the shared skill-expansion option in the sidecar

Replaces the sidecar's workflow-detection dance with core's
expandSkillCommands option and gates on isSkillsToolAvailable, restoring
textual expansion where the tool is missing (yolo mode or the skills tool
toggle) — a gap in the previous desktop-only change.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): drop the display inverter for expanded transcripts

Accepted trade-off to keep the change minimal: sessions recorded before
skills switched to the skills tool, workflow sends (deprecated), and
yolo-mode skill sends persist expanded instructions and now render that text
as-is instead of being collapsed back to the typed /command at projection
time.

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>

* Use fixed selection chevron in account dialog to match other dialogs (#13364)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): align system prompt with session mode (#13361)

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): finalize queued turns on chat_done with canonical history reconcile (#13330)

Turns that settle through the event stream (queued prompts, including the
first prompt of a fresh session) resolve their send() RPC early, so nothing
cleared the streaming shimmer or reconciled live-streamed content against
the persisted transcript at turn end. A turn whose deltas were incomplete
stayed visually streaming forever and only healed when a later non-queued
send rehydrated history.

chat_done (and chat_session_ended / the queue-drain double check) now clears
the active assistant streaming id and schedules a short-delayed
read_session_messages + applyCanonicalHistory, guarded by turn epoch,
session id, and in-flight send submissions so it never clobbers a newer
turn or duplicates the blocking send path's own finalization.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.14

* fix(clients): filter non-chat models from chat pickers (#13317)

* fix(clients): filter non-chat models from chat pickers

* fix(clients): align chat model eligibility

* fix(desktop): strip user_input envelope when copying a user message (#13369)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>

* docs: show DeepSeek V4 peak and off-peak pricing (#13312)

* docs: update DeepSeek V4 average pricing

* docs: show DeepSeek peak and off-peak pricing

* docs: add GLM-5.3 reference pricing (same as GLM-5.2)

* docs: add GLM-5.3 to ClinePass models table

* fix(llms): display billed gateway cost (#13385)

* fix(shared): run PowerShell commands with fail-fast error semantics (#13358)

* fix(shared): run PowerShell commands with fail-fast error semantics

The run_commands PowerShell wrapper never set $ErrorActionPreference, so
the default 'Continue' applied: a pipeline erroring per item (e.g. a
malformed Where-Object over Get-ChildItem -Recurse) emitted one error
record per enumerated file - tens of thousands of stderr records on
large trees, looking like a hang - and could still resolve as SUCCESS
with exit 0.

Prepend $ErrorActionPreference='Stop'; to the script content executed
by the ScriptBlock so the first error terminates the command with a
non-zero exit and a single error message. Concatenated on the same line
as the user command so error line numbers stay unshifted.

Fixes #13285

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(shared): set the fail-fast preference in the bootstrap scope

Setting $ErrorActionPreference='Stop' by string-prepending it into the
scriptblock source displaced a leading param(...) from its mandatory
first-statement position, so scripts beginning with a param block failed
with CommandNotFoundException. Preference variables are dynamically
scoped, so setting Stop in the -Command bootstrap gives the invoked
scriptblock identical fail-fast semantics while keeping the user script
byte-identical (param works, error positions unshifted) and drops the
doubled-quote escaping.

* docs(shared): document the fail-fast tradeoffs in the PowerShell wrapper

Stop promotes every non-terminating error, not only per-item pipeline
floods: partial-result commands (recursive listings over access-denied
junctions) now stop at their first error, and Windows PowerShell 5.1
turns in-script stderr redirection of succeeding native commands fatal.
State this in the wrapper comment as a deliberate tradeoff, with the
GitHub Actions precedent and the per-command opt-outs.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* ci: stop over-long changelogs from silently dropping release Slack posts (#12955)

Slack section blocks reject text longer than 3000 characters. The Slack
action logs that rejection as ##[error] but does not fail the step, so an
over-long changelog drops the release announcement while the run stays
green — cline@3.0.50 (3272 chars) published to npm, tagged, and cut a
GitHub release with no Slack post and nothing red to notice.

Every publish workflow pasted the changelog section verbatim into one
section block, so all six were exposed; the SDK, desktop, and extension
sections were only 150-350 chars under the ceiling.

Add a slack_content output alongside content: unchanged when the section
fits, otherwise trimmed on a line boundary with a link to the full
release notes. Only the Slack payload uses it — GitHub release bodies and
the desktop updater manifest still get the whole section.

* ci: tidy workflow cache config and job permissions (#13403)

Publish workflows now always do clean npm installs (no dependency
cache in their test gates), the e2e workflow's cache keys are
exact-match only, and the e2e job drops an id-token permission it
never used.

* Rename desktop app from "Cline Code" to "Cline" (#13401)

* Rename desktop app from Cline Code to Cline

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Format touched Rust test assertions

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): surface provider-executed tool activity as observational events (#13300)

* fix(llms): surface provider-executed tool activity as observational events

Provider-executed tool parts (e.g. every tool the Claude Code CLI runs
inside its own session) were dropped by the model-tool guard added for
web search: only declared model tools were re-emitted, everything else
hit continue with nothing yielded. Those sessions modified the workspace
with no tool activity in runtime events, transcripts, or the UI.

Route all providerExecuted parts onto the observational path instead:
emit execution-tagged tool-call-delta and tool-result events, matched by
tool-call ID for providers that omit the flag on the result half. They
stay out of AgentRuntime's execution/approval loop, and the runtime
already persists them as modelToolActivities and projects them for
display.

The AgentModelEvent tool-result variant widens toolName from
ModelToolName to string to carry the provider's own tool names.

* fix(agents): keep turns that are only provider-executed tool activity

A turn consisting solely of observational tool activity has an empty
assistant content array - the activity lives in message metadata, since
projecting it into content would replay tool_use blocks the model never
gets results for. The empty-content guard threw on such turns, erroring
the run and losing the activity from the transcript. Count model-tool
activity as content for the emptiness check (error finishes still
throw); replay stays safe through the codec's empty-content placeholder.
Also drop the trailing text delta from one gateway test so the tool-only
stream shape stays covered end to end.

* feat: allow agents to create scheduled tasks (#13331)

* feat(core, desktop): add durable todo agenda

* fix(desktop): secure todo approvals and track tool usage

* fix(desktop): clean up failed approval delivery

* fix(desktop): authenticate approval connections

* fix(desktop): cancel approvals on broadcast failure

* fix(desktop): authenticate development approvals

* fix(desktop): harden development approvals

* test(core): make task paths cross-platform

* fix(desktop): serialize approval readiness

* refactor(core): unify todo and schedule tools

* feat(core): distinguish user todos from agent suggestions

* fix(core): hide tasks tool in yolo mode

* fix(core): enforce schedule workspace scope

* fix(core): bind schedule scope to hub connection

* fix(core): establish task scope at hub startup

* fix(core): scope task automation by workspace

* test(core): normalize workspace path expectations

* test(core): serialize Windows CI workers

* fix(core): reject unregistered schedule authority

* fix(desktop): guard task execution commands

* fix(core): avoid polynomial regex in mention parsing

* fix(core): address schedule tool review feedback

* fix(core): bind websocket clients to hub workspace

* fix(core): flatten tasks tool input schema

* fix(core): authorize multi-workspace hub clients

* test(core): type hub transport authority mock

* fix(cli): register a workspace client for remote schedule commands (#13398)

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): treat ClinePass as OAuth-managed in the chat credential gate (#13404)

* fix(desktop): treat ClinePass as OAuth-managed in chat credential gate

ClinePass shares the Cline account OAuth credentials (its auth handler
stores under the "cline" provider), so the webview never sees a plain
API key for it. The chat pre-flight check only exempted cline/oca/
openai-codex, so switching to ClinePass while signed in via OAuth
blocked with "Missing API key" even though the sidecar resolves the
stored access token fine (which is why the CLI worked).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: format helpers.test.ts with biome

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): stack code block lines when streamdown lineNumbers is off (#13412)

streamdown renders each Shiki token line as a bare inline span with no
newline text between non-empty lines, and only applies its block line
class when lineNumbers is on. With lineNumbers off (the desktop app's
config) every multi-line fenced block collapsed into one run-on line.
Make the direct line spans under code-block-body display: block in the
shared markdown.css; empty lines keep their height via their lone "\n"
child under white-space: pre.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): work summary undercounts wall time when pre-tool thinking attaches to the answer (#13413)

* fix(desktop): anchor work summary duration on the answer row, not attached pre-tool reasoning

The collapsed 'Worked for Xs' row undercounted wall time whenever a turn's
assistant message contained thinking + tool_use with no narration text: the
canonical projection emitted the reasoning-only row after the tool row (both
stamped before the tool executed), the webview attached that row to the final
answer, and collapseCompletedWork used the answer's earliest attached
reasoning timestamp as the end anchor - excluding the entire tool execution
(e.g. 'Worked for 5s' for a turn with an 8s command).

- webview: end the work span at the answer row's own timestamp, clamped to
  the last collapsed row so a fallback answer bubble with a synthetic early
  timestamp cannot shrink the duration either
- sidecar: flush pending thinking before a tool_use row so rehydrated
  transcripts keep the live-stream order (thinking before its tool call) and
  pre-tool reasoning no longer rides on the next answer

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep interleaved thinking between the tool calls it separates

Address Greptile review: when one assistant message interleaves thinking
between multiple tool_use blocks, each reasoning segment now projects at its
own position (attached to a text row from its own segment when present,
otherwise as its own row) instead of merging into the first reasoning row,
which displayed later thinking before a tool call it actually followed.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): remove settings gear hover state while Account screen is open (#13408)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't show "No sessions found" while session history is still loading (#13414)

* fix(desktop): don't show 'No sessions found' while session history is still loading

Replace the isLoadingHistory flag with hasLoadedHistory, set only once the
backend has actually answered a list_discovered_sessions request. The sidebar
and Sessions view now keep their loading state until that first definitive
response, so the empty-state copy can no longer appear while history is still
being fetched (or while a failed fetch is being retried).

Also retry a failed initial fetch on the 2s event cadence instead of stranding
the UI until the 12s periodic poll, which is what stretched the misleading
empty state to ~10 seconds after a webview reload when the websocket lost the
race with the page load.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop history fast-retry from re-arming after hook unmount

A failed initial fetch that settles after the hook unmounted could schedule a
new retry timer after cleanup had already cleared the refs, leaving the
abandoned hook polling the backend every 2s. Guard scheduleRefresh with a
disposed ref set by the mount effect's cleanup.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix @ file mentions breaking on paths with spaces (#13391)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with "command not found" on VS Code 1.134 (#13402)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with 'command not found' on VS Code 1.134

Code action commands carried arguments (expandedRange, diagnostics),
which routes them through VS Code's CommandsConverter cache. VS Code
1.134 disposes the cached entries before the clicked action executes,
so every lightbulb action failed with 'Actual command not found,
wanted to execute cline.addToChat'.

Drop the arguments so the command id is passed through directly, and
recover the context in the handler instead: getContextForCommand now
expands an empty selection by 3 surrounding lines (matching the old
provider behavior) and gathers document diagnostics intersecting the
range when none are passed explicitly.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Scope gathered diagnostics to the selection/cursor

Match the old CodeActionContext.diagnostics behavior: only include
diagnostics intersecting the range the action was requested for, not
the surrounding lines the text gets expanded to.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop: unify Plugins, MCP, and Skills into one Plugins hub with a dedicated Marketplace page (#13411)

* Unify desktop plugins, apps, MCP, and skills into one Plugins hub with a Browse directory mode

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Open the marketplace directory as a modal over the Plugins hub instead of swapping the page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename directory to Marketplace: Browse Marketplace button, Marketplace modal title with icon, search placeholder

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix search input focus ring clipped by the Marketplace modal scroll container

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Address Greptile review: keep selected tag chip visible when its count drops to zero, and remount installed tab when a marketplace install completes after the modal closed

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Track marketplace modal mutation flag in a ref so a close click racing a queued render cannot skip the inventory remount

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make Marketplace its own settings page under Customizations and restore Channels as a standalone page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Remove icon from Marketplace page header for consistency with other settings pages

* Notify mounted inventory views when the marketplace invalidates the cache so late install completions refresh the Plugins hub

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop/ui): recommended and free model tiers in the composer model selector (#13410)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK (#13415)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): recommended-feed badges and descriptions in provider settings (#13416)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* feat(desktop): recommended-feed badges and descriptions in provider settings

Review suggestion on #13410: the provider settings page has room for
more model detail than the composer's picker. The cline/cline-pass
provider cards now refresh their model list through
list_provider_models (the catalog snapshot deliberately skips the
recommended-feed overlay so the startup catalog fetch never blocks on
the feed) and render Recommended/Free tier badges plus feed tags (NEW)
next to the model name, with the model description underneath. The
refreshed list also surfaces the live entries instead of the bundled
snapshot.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): scope the settings featured model list to its provider and revision

The fetched featured list was unscoped component state: switching
between cline and cline-pass reused the component instance, so the
previous provider's models stayed visible while the new request was
pending (or forever, when it failed), and the retained copy shadowed
later provider.modelList updates — adding a second custom model
submitted the stale list as the complete configuration and dropped the
first addition.

The fetched list now only applies to the provider and modelList
revision it was fetched for (falling back to the catalog snapshot
otherwise and refetching on membership changes), and add-model submits
the union of the displayed and configured ids so an update can never
silently unconfigure existing entries.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): update packed-Tailwind smoke contract for the picker's max-h-64 (#13421)

The ui-publish smoke check pins a set of Tailwind candidates the packed
sources must emit; #13410 grew the SearchCombobox options list from
max-h-56 to max-h-64, so the publish run failed on the stale candidate.
All other pinned candidates verified against the current sources.

* feat(desktop): refresh app icons and branding (#13400)

* ci(vscode): upload E2E failure recordings from the right path (#13427)

The job sets working-directory: apps/vscode, but that default applies to run
steps only, not to `uses:` steps. Since #10961 moved the extension under apps/
and added that default, the artifact path has resolved against the repo root,
matched nothing, and every failing run logged "No files were found with the
provided path: test-results/playwright/" instead of uploading recordings.

Widen to test-results/ so Playwright's error-context snapshots ship alongside
the videos.

* fix(hooks): deliver tool hook contextModification to the model (#13297)

* fix(hooks): deliver tool hook contextModification to the model

On the next engine, a tool_call (PreToolUse) hook's contextModification
was parsed into HookControl.context and then silently dropped: the
runtime beforeTool/afterTool result contract had no channel for
injecting conversation context. Legacy consumed it (ToolExecutor /
ToolHookUtils pushed <hook_context> blocks into the next user turn), so
this was a regression of documented behavior.

- Add appendContext to AgentBeforeToolResult/AgentAfterToolResult.
- AgentRuntime collects appendContext across hooks during an
  iteration's tool executions and appends one <hook_context> user
  message after the tool results, keeping tool-result parts contiguous.
- Map HookControl.context into appendContext in both subprocess hook
  layers (skipped when the hook cancels, matching legacy, where the
  message doubled as the error).
- Truncate injected context at 50KB per hook output, matching legacy.
- Concatenate appendContext across merged hook layers.

tool_result (PostToolUse) hooks still run detached with stdout ignored;
making them blocking so their context can be collected is a follow-up.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): stamp tool identity on injected hook context blocks

Contexts are batched into one message after the tool results, and
parallel tool execution collects them in completion order, so position
alone cannot attribute a block to its tool call. Add tool_name and
tool_call_id attributes to each <hook_context> block.

* fix(hooks): sanitize hook context block markup

Attribute values (tool_name, tool_call_id) are stripped of quote/angle
characters and embedded </hook_context> closers in hook output are
neutralized, so neither provider-supplied ids nor hook text can corrupt
or spoof a block's stamped identity.

* fix(hooks): neutralize forged opening hook_context tags in hook output

The previous sanitization only neutralized closing tags, so hook output
could still open a forged <hook_context> block claiming another tool's
identity. Escape both opening and closing embedded tags with one rule.

* fix(hooks): hide injected hook context from user-facing transcripts

Stamp the injected hook-context user message with displayRole 'system'
(the compaction-summary convention) so it reaches the model but does
not render as a user bubble in live or replayed transcripts. Without
this, resuming a session showed the raw <hook_context> block as if the
user had typed it.

* fix(hooks): neutralize case-variant embedded hook_context tags

The tag-neutralization regex was case-sensitive, so hook output could
still smuggle a forged tag as <HOOK_CONTEXT>. Match case-insensitively.

* fix(vscode): map PreToolUse contextModification into runtime appendContext

The extension's hooks adapter bridged file hooks into the SDK runtime
but forwarded only cancel/errorMessage, so a PreToolUse hook's
contextModification never reached the model. Map it into the runtime's
appendContext channel; HookFactory already truncates it at 50KB.

* fix(vscode): hide hook-injected context from replayed transcripts

Live sessions never rendered the injected <hook_context> user message,
but session reload replayed it as a user bubble (and post-resume turns
kept doing so). Treat these messages as synthetic in the user-message
mapping: honor the displayRole 'system' stamp the runtime sets, with a
text-prefix guard for paths where metadata is unavailable. This also
keeps edit/regenerate ordinal mapping aligned with visible bubbles.

* fix(hooks): run file hooks through exactly one layer per host

The VS Code extension registered two independent hook execution layers:
its own hooks adapter (config.hooks) and the SDK core's file-hook
extension from the runtime bootstrap. When both discover the same hook
files, every hook executes twice per event — and with context injection
wired, each contextModification would be injected twice.

Add a 'hooks' runtime config extension kind (in the default set, so the
CLI keeps core file hooks unchanged) and gate the bootstrap's file-hook
extension on it. The extension excludes 'hooks' at session start, so
its adapter — which also provides the hook status UI and the
hooksEnabled setting — is its single execution path.

* fix(vscode): discover hooks from the session workspace, not only global state

Hook discovery read workspaceRoots from global state shared across
every Cline instance, so another window repointing it made workspace
hooks silently stop being discovered. With the extension's adapter now
the single hook execution layer, that meant no hooks at all.

HookFactory takes an optional sessionWorkspaceRoot and unions that
root's .clinerules/hooks into discovery (and into cwd resolution), fed
from the session config's cwd. Shared-state discovery still works, so
behavior in the single-window case is unchanged.

* fix(hooks): keep sanitized hook attribute values distinguishable

Replacing every markup delimiter with the same underscore could
collapse two tool call ids that differ only by such a character into
identical stamps. Escape each delimiter with a distinct token instead.

* fix(hooks): make hook attribute sanitization injective

Escaping the underscore itself turns the attribute escaping into a
uniquely decodable code, so no two distinct tool call ids can collapse
to the same sanitized stamp (previously an id containing a literal
escape token could collide with an id containing the delimiter).

* fix(vscode): reconstruct hook status rows when replaying transcripts

hook_status messages are emitted live but never persisted, so reloading
a session dropped every hook row. The injected <hook_context> blocks
carry the hook source and tool name, so the replay translator now
rebuilds a completed hook status row from each block. The injection is
also no longer treated as a user turn boundary, so the final turn's
completion retag is unaffected by it.

* fix(hooks): collect PostToolUse hook output and honor its control (#13298)

* fix(hooks): collect PostToolUse hook output and honor its control

tool_result (PostToolUse) hooks ran fire-and-forget with stdout
ignored, so their entire JSON output — contextModification and cancel —
was discarded. Legacy awaited PostToolUse, injected its
contextModification into the conversation, and honored cancel.

- Run tool_result hook commands blocking (same 120s default timeout as
  tool_call) in both the hook-config-file layer and the agent-hook
  subprocess layer.
- Map their output: cancel stops the run with the hook's error message
  as the reason; otherwise context is injected via afterTool
  appendContext.

This restores legacy blocking semantics: tool results now wait for
tool_result hooks, but only in sessions that have one configured.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): bound tool_result hook wait and isolate cancel reason

Address review findings:
- The agent-hook subprocess layer forwarded an unset timeoutMs
  unchanged, so a tool hook command that never exits would block the
  agent indefinitely. Default both tool_call and tool_result to the
  120s bound the hook-config-file layer already used.
- A cancelling hook's error message was folded into the same context
  field as other hooks' injectable context, so merging controls could
  leak unrelated hook context into the cancellation reason. Carry it as
  a separate cancelReason, and surface it as the stop reason for
  beforeTool cancels too.

* fix(hooks): prefer errorMessage as a cancelling hook's stop reason

When a cancelling hook returns both contextModification and
errorMessage, the context-first parse precedence made the injectable
context the cancel reason and discarded the actual error. Parse the two
fields separately: errorMessage wins as the cancel reason (matching
legacy), and a lone errorMessage still folds into injectable context
for non-cancelling hooks as before.

* fix(vscode): honor PostToolUse hook cancel and contextModification

The adapter awaited PostToolUse hooks but discarded their output
entirely. Map cancel to a stop control (with errorMessage as the
reason) and contextModification into the runtime appendContext channel,
matching the PreToolUse mapping and legacy semantics.

* fix(hooks): whitespace-only errorMessage no longer suppresses the cancel reason

A cancelling hook returning meaningful context alongside a blank
errorMessage lost both: the parsers selected the whitespace as the
reason and the result mappers trimmed it away. Require a non-blank
errorMessage before it wins, so context serves as the fallback reason.
Apply the same fallback in the extension adapter's stop mapping.

* fix(core): stop Windows CI worker crashes from the agenda spec watcher (#13428)

* fix(core): watch agenda task specs via the resolved long path

fs.watch on a path with 8.3 short components (e.g. C:\Users\RUNNER~1
temp dirs) trips a libuv assertion in fs-event.c on Windows and aborts
the whole process. Since the agenda task manager landed, every hub
server test spins up its spec watcher on such a path on hosted Windows
runners, killing the vitest worker and failing the sdk-test Windows job
on every branch. Resolve the specs dir with realpathSync.native before
watching so libuv only ever sees the long form.

* test(ui): stub ResizeObserver for @pierre/diffs in tool-diff tests

jsdom does not implement ResizeObserver, so every ToolFileDiff render
logged a ReferenceError from @pierre/diffs to stderr. Tests still
passed; this just silences the noise the same way the constructable
stylesheet shim does.

* fix(core): skip the agenda spec watcher when the dir does not resolve

Falling back to the raw path on realpath failure would reintroduce the
Windows short-path abort; log and go without the watcher instead.

* fix(vscode): honor the classic truncation range when migrating legacy tasks (#13419)

Classic Cline truncated long conversations by omitting an index range of
api_conversation_history from every API request (keep the first
user-assistant pair, drop everything through the range end, strip
orphaned tool_results from the first kept message). The range was
persisted on the history item while the full history stayed on disk.

legacyApiHistoryToSdkMessages ignored conversationHistoryDeletedRange
and converted the entire file, so resuming a migrated long task handed
the SDK an untruncated working context that could exceed the model's
context window by millions of tokens - every request failed with
'prompt is too long' and every compaction restarted from the full
history (#12996, confirmed by the reporter: the task was migrated from
an older version and broke after a restart, with each compaction
starting from ~3M tokens).

The migration now replays exactly what the classic extension sent:
slice out the deleted range and drop orphaned tool_results, mirroring
ContextManager.getTruncatedMessages (see origin/main). Malformed ranges
fall back to the full history (previous behavior).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): show the diff edit view for multi-line edits in CRLF files (#13417)

The edit preview computed proposed content with an exact old_text match, but
the SDK executor normalizes old/new text to the file's own line endings before
matching (#12305) - reads strip CR, so models emit LF-only text even for CRLF
files. Any multi-line old_text in a CRLF file therefore failed the preview's
match: the diff edit view silently never opened while the executor applied the
edit. Single-line edits (no line break in old_text) were unaffected, which is
why the diff view appeared to trigger inconsistently.

Mirror the executor's EOL normalization (and its literal $-sequence insertion)
in the preview computation.

Fixes #13296

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): report truthful session status so desktop checkpoint restore stops wedging (#13418)

* fix(core): keep hub session status truthful across queue-drained turns

Queue-drained turns settle only through the event stream, but the hub
runtime host mistranslated their lifecycle in two ways:

- session.updated events carrying only a snapshot (persistence updates)
  defaulted the projected status to "running". When one trailed the
  final idle update after a turn, clients that track busy state from
  status events (the desktop sidecar's workspace restore gate) stayed
  busy forever. Use the snapshot's real status and emit nothing when
  neither source reports one.
- the per-run agent.done dedup was only reset by run.started, which the
  daemon-side queue drain never publishes, so a drained turn's done was
  swallowed as a duplicate of the previous turn's. Reset the dedup on
  session.pending_prompt_submitted, and suppress stale run.completed
  events that land inside a drained turn's window so they can neither
  emit a phantom done nor consume the drained turn's dedup slot.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(desktop): cover restore unlock after an event-settled queued turn

Exports the sidecar's core-session event handler so the queued-turn
lifecycle (busy via status events, cleared by the done agent event,
restore allowed afterwards) is testable end-to-end.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): start interactive sessions without a prompt as idle

The runtime host reported every new session as "running" until its
first turn ended. Interactive hosts (the desktop app) start sessions
with no prompt and dispatch turns through separate send calls, so a
created-but-never-prompted session stayed "running" forever — wedging
clients that gate workspace operations (checkpoint restore, message
edit) on active turns.

Interactive no-prompt starts now begin idle, start emits the session's
actual status (resumed sessions no longer masquerade as running), and
markTurn* transitions keep tracking in-memory status for lazily
persisted sessions so the first turn still reports running -> idle.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: format hub-runtime-host test filter

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop the drained-turn done bookkeeping, keep the minimal fix

The stuck restore is fully explained by the two status defects (fabricated
"running" from snapshot-only session.updated events, and never-prompted
interactive sessions reporting "running"). The done-dedup machinery for
queue-drained turns addressed a separate cosmetic gap (queued turns emit no
chat_done, pre-existing) and required fragile run-window heuristics, so it
is removed to keep this change reviewable. Sidecar test now settles the
queued turn through the status event, matching the shipped mechanism.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs(sdk): document the truthful session-status contract

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(deps): update Langfuse packages and bump app versions (#13443)

* chore(deps): update Langfuse packages and bump app versions

Update @langfuse/otel to v5.10.1 and add @langfuse/vercel-ai-sdk v5.9.1 for improved observability with Vercel AI SDK.

Bump versions for @cline/code to 0.0.14 and @cline/ui to 0.2.0-next.6, updated via bun.lock.

Other Changes:
Added optional userId to AgentRuntimeConfig.
Propagated userId, sessionId, conversationId, runId, iteration, provider, and model context into AI SDK telemetry.
Added AI SDK 7 runtimeContext with explicit includeRuntimeContext.
Added stable OTEL_SERVICE_NAME=cline-sdk.
Added runtime metadata assertions in agent tests.

* add taskId

* Revert "add taskId"

This reverts commit f20d31d96d.

* docs: simplify Open Cline step in installing guide (#13405)

* docs: remove duplicate GLM-5.3 rows in ClinePass tables (#13449)

Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>

* chore(sdk): release v0.0.76

* chore(cli): release v3.0.56

* docs(cli): scope the v3.0.56 release notes to CLI-visible changes

* feat(desktop): interactive welcome hero graphic (#13399)

* feat(desktop): add interactive welcome hero

* feat(desktop): support composable welcome hero variants

* feat(desktop): reskin first-run onboarding (#13441)

* refactor: centralize client tool availability (#13451)

* chore(sdk): release v0.0.77

* docs(cli): drop the tasks tool from the v3.0.56 notes, it is desktop-only

* chore(vscode): prepare 4.1.11 release

* chore(desktop): release v0.0.15

* fix(vscode): remote config MCP settings (#13466)

* fix(vscode): enforce enterprise MCP controls on the Customize marketplace

The unified Customize marketplace replaced the old MCP marketplace
without carrying over enterprise remote-config enforcement: the catalog
RPC returned every MCP entry and installs were never policy-checked,
so orgs with mcpMarketplaceEnabled=false or an allowedMCPServers
allowlist saw (and could install) all marketplace MCP servers.

- Filter MCP entries out of getMarketplaceCatalog when the marketplace
  is disabled, and restrict entries to the allowlist when configured
  (matching entry id, display name, installed server name, or source
  repo URL, mirroring legacy GitHub-URL allowlist ids)
- Reject installMarketplaceEntry requests that violate the policy
- Map the published catalog's repo/homepage fields onto
  sourceUrl/homepageUrl so URL-based allowlists can match
- Update the enterprise MCP server controls docs

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: simplify MCP marketplace policy enforcement

Fold the policy check into marketplace-helpers, drop the dedicated
test suite, and trim the docs edit to the strictly necessary line.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Treat an empty preserved capability list as unspecified when seeding tools (#13465)

* Treat an empty preserved capability list as unspecified when seeding tools

toSdkModelInfo guarded the tools seeding with a strict
preservedCapabilities === undefined check, but modelHasCapability —
the runtime's own reader — treats undefined AND length === 0 as
"unspecified". A custom OpenAI-Compatible model whose stored
capabilities field is a defined-but-empty array (a config carried over
from before the field existed, or one round-tripped through a boundary
that defaults it to []) skipped the seeding; the first boolean
projection to run afterwards (e.g. supportsReasoning) then populated
the array, the runtime gate read the non-empty, tool-less list as
authoritative, and every tool definition was silently dropped from the
session (#13463).

The guard now covers the empty array too, matching the reader's
unspecified semantics.

* test: satisfy the store's isModelInfo gate so the empty-capabilities case actually reaches knownModels

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: yzxcj797 <yzxcj797@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>

* chore(vscode): prepare 4.1.12 release

* Add feature flags to the desktop app (#13289)

* Add feature flags to the app

* React to account updates

* Address comments

* use a per-app file

* fix: propagate Langfuse session telemetry (#13473)

* fix telemetry session propagation

* feat telemetry client version metadata

* fix(core): address Langfuse review feedback — hub client identity + delegated agent session grouping (#13475)

* fix(core): rebuild hub session client identity from request headers

Hub-backed sessions do not transport extensionContext (it is local-only),
so the daemon's runtime built traces without the clientName/clientVersion
metadata even though the hub client bakes X-CLIENT-TYPE / X-CLIENT-VERSION
into the session's provider headers. Reconstruct extensionContext.client
from those headers during local runtime bootstrap so hub-backed Langfuse
traces carry the same client identity as local runtimes, and the daemon's
header re-resolution stops clobbering the original X-CLIENT-TYPE.

* fix(core): propagate parent distinctId/sessionId to delegated agents

Delegated agents (spawned sub-agents, configured agents, teammates) were
built without distinctId and sessionId, so their Langfuse traces had no
userId or sessionId and did not group with the parent user or session.
Thread the host-resolved distinctId through RuntimeBuilderInput and the
root sessionId through the delegated-agent config provider, and copy both
onto the delegated AgentConfig.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* ci(vscode): make combined nightly manual-dispatch only

The PublishNightly environment gained required reviewers, so each cron
run parked on approval, held the workflow's concurrency group, and
silently cancelled every scheduled run queued behind it. 20 consecutive
scheduled nightlies died this way between 2026-07-31 and 2026-08-21;
the only nightlies that shipped in that window were manual dispatches.

Drop the cron rather than leave a trigger that cannot succeed unattended.

* feat(hub): add drain and upgrade commands with replay support (#13468)

* feat(hub): add drain and upgrade commands with replay support

* handles disconnection

* feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport

Completes the wiring the previous commits' primitives needed:
HubServerTransport gains isDraining(), hub.drain/hub.status/profile.get
command handling, and replayEventsAfter() (backed by the durable event
log), plus the sequence/sinceSequence wire types they depend on in
shared/hub.ts. run-queue-handlers.ts reads the active bot profile's
plugin roots when executing durable runs.

Also adds hub/profiles/: profile.json (identity/rules/plugins) ->
system prompt composition, --profile / CLINE_HUB_BOT_PROFILE
resolution, and the bundled cline-dad profile with its
cline_hub_support read-only diagnostics tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Revert "feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport"

This reverts commit 6696d5d202.

* fix(hub): dedupe replayed events by eventId, not just sequence

HubEventLogStore.append() returns a new envelope stamped with a
sequence rather than mutating the input, so a pending approval
re-issued sequence-less by subscribe() (it predates any durable-log
append) and its later sequence-stamped copy from the durable log are
two different objects carrying the same eventId. The replay-then-live
buffer in browser-websocket.ts only deduped by sequence, so the
sequence-less copy's guard never tripped and it was delivered a second
time when the buffer flushed after replay.

Track delivered eventIds alongside the sequence cursor; eventId
survives the append/stamp round-trip unchanged, so this dedupes the
exact-same logical event regardless of which copy arrives first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): wire drain, durable event log, and run queue into the live transport

CI on this branch failed bun run build:sdk: browser-websocket.ts,
client/index.ts, and hub-websocket-server.ts (already on this branch)
reference sequence/sinceSequence, HubServerTransport.isDraining(), and
the "hub.drain" command — but the commit that reverted bot profiles
out of this branch also reverted this wiring, since it shared a commit
with the profiles work. That wiring is a hub concern, not a
bot-profiles one; split it back out.

- shared/hub.ts: sequence/sinceSequence types, run.enqueue/run.list/
  hub.drain/hub.status/stream.replay capability, command, and event
  names. profile.get intentionally excluded — stays bot-profiles-only.
- context.ts: isDraining() on HubTransportContext. botProfile field
  intentionally excluded.
- hub-server-transport.ts: eventLog/runQueue fields and start/stop
  lifecycle, publish() appends to the durable log, handleCommand cases
  for run.enqueue/run.list/hub.drain/hub.status, drain-refusal check,
  replayEventsAfter()/lastEventSequence(). startBotProfile()/
  startHubSupportTool() and the profile.get case intentionally
  excluded.
- run-queue-handlers.ts: added without handleProfileGet (needs
  ctx.botProfile, which doesn't exist here).
- hub-upgrades.test.ts: added without its two bot-profile-injection
  tests (they need a resolved bot profile to assert against).

Verified bun run build:sdk exits 0 (the exact CI command) and
bunx vitest run src/hub passes (311/312; the one failure is the
same pre-existing environment-timing flake already present before
this change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): export instance-lock, event-log, and run-queue from the hub barrel

These landed as internal modules only; hub-server-transport.ts and
hub-websocket-server.ts import them by direct path, but nothing
re-exported them from the public @cline/core/hub surface the way
sibling discovery/server modules already are.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): wire the instance lock into the daemon entry point

The singleton lock (discovery/instance-lock.ts) and its consumption in
startHubWebSocketServer/ensureHubWebSocketServer were already on this
branch, but the daemon entry point's own half was not: retrying a bind
when a retiring predecessor still holds the lock, and exiting with a
distinct code (3) instead of the generic fatal path when a live Hub
already owns the data directory. Without this, a daemon racing a
retiring predecessor could fail outright instead of waiting the lock
out, and losing the singleton race looked identical to a crash.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): address drain/upgrade review findings (#13478)

- cline hub upgrade: check idleness at least once (--wait 0 works), reject
  non-numeric --wait, and un-drain on every abort path so an aborted
  upgrade can never leave the hub refusing new work
- add cline hub drain --off and the off query param to requestHubDrain so
  POST /drain?off is reachable from shipped code
- HubEventLogStore/HubRunQueue: WAL journal mode + busy_timeout, and stamp
  sequences from lastInsertRowid instead of SELECT MAX(sequence)
- HubInstanceLock.acquire: degrade to an unheld lock when SQLite is
  unavailable instead of refusing hub startup; only BUSY/LOCKED still
  raises HubLockHeldError
- ensureHubWebSocketServer: retire an unusable discovered hub through the
  shared retireDiscoveredHub (busy hubs are attached to, drain precedes
  shutdown, discovery cleared only when the hub actually retired)
- replay adapter: advance the cursor past eventId-deduped events, cap
  replay pages, stop when the cursor stalls, and drop the dedupe set after
  the buffered flush so it cannot grow for the socket lifetime

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* fix(hub): derive the singleton e2e challenger cwd portably

The challenger's working directory was derived by round-tripping the
discovery path through a file: URL and stripping the last pathname
segment. On Windows that yields a POSIX-style '/C:/...' path, which is
not a valid spawn cwd, so the spawn fails ENOENT before the singleton
lock is ever contested and the Windows SDK test job goes red.

The data dir is simply the discovery file's parent: use dirname().

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop stored capability lists from silently revoking tool calling for custom models (#13476)

* fix(core): seed tools capability when custom model capabilities are synthesized from boolean flags

For a models.json entry with no explicit capabilities list, toStoredModelInfo
synthesized a capability array purely from boolean convenience flags (e.g.
supportsReasoning: true -> ["reasoning"]). modelSupportsToolCalling fails open
only for a missing or empty list, so the synthesized non-empty list read as an
authoritative denial and silently stripped every tool definition from requests
to custom OpenAI-compatible models (#13463).

Seed "tools" whenever the list was not explicitly authored and the boolean
projections made it non-empty, preserving the fail-open contract. Explicitly
authored capability lists remain authoritative and can still disable tools.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(core): cover stale catalog capability overrides

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: treat stored capability lists as non-authoritative for tool calling

The hasExplicitCapabilities guard still let two producers of tool-less
lists through:

- The VS Code legacy-override migration (legacyModelInfoToOverrides)
  persists explicit partial lists like ["prompt-cache"] into models.json
  for custom OpenAI-compatible models, which then read as an authoritative
  "cannot call tools" and drop every tool - same symptom as #13463.
- Any hand- or UI-authored partial list on a non-catalog model.

Stored entries and user-authored provider metadata have no way to declare
"cannot call tools" (there is no supportsTools field, and every writer
that authors a full list includes "tools"), so seed "tools" into any
non-empty list for a language model. Only generated catalog capabilities
remain authoritative - a genuine no-tools catalog model stays that way -
and non-language models (e.g. image generation) never gain a tools claim.

Also make legacyModelInfoToOverrides write "tools" into the arrays it
fabricates, matching the providers.json migration, so models.json stops
being poisoned for older readers.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): prepare 4.1.13 release

* chore(sdk): release v0.0.78

* chore(cli): release v3.0.57

* fix(core): run hub e2e files serially so daemon timing budgets survive CI contention

singleton.e2e.test.ts (added in #13468) spawns real daemons and runs for
~15s. Vitest's default file parallelism let it run alongside
shutdown.e2e.test.ts, whose assertions are wall-clock bound: discovery
within 10s, exit within 5s, and a 2s shutdown watchdog. On the 2-core
windows-latest runner that contention alone broke those budgets, failing
the shutdown test two different ways across runs — once never observing
discovery, once with the daemon forced to exit before its HTTP 202
flushed (socket hang up). The test passed on Windows before #13468 and
has failed every SDK publish run since.

* chore(desktop): release v0.0.16

* test(sdk): give windows-sensitive suites realistic timeouts

Four consecutive SDK publish runs failed on windows-latest, each on a
different test, all of them plain timeouts: two @cline/shared SQLite
tests at the 5s vitest default, core's bash executor at 10s, and the hub
singleton endpoint test at 10s. The 2-core Windows runner spawns forks
and takes SQLite locks slowly enough to blow those budgets under load.

These timeouts guard against hangs; they are not timing assertions (the
one suite that does assert elapsed time, shutdown.e2e, was fixed by
removing file-level parallelism instead). Raise core to 20s and give
@cline/shared an explicit 15s in place of the inherited 5s default.

* fix(telemetry): emit task.completed from every session teardown path (#13489)

The task.completed fallback lived only inside shutdownSession, but
stopSession/dispose route interactive sessions with a terminal reported
status through releaseSessionRuntime, which never emitted. Truthful
session-status reporting (shipped in 4.1.11) re-routed a large share of
interactive stops onto that branch and silently dropped the event.

Route the emission through a single choke point,
emitTaskCompletedOnTeardown, called from both shutdownSession and
releaseSessionRuntime. The completion criterion no longer reads
session.status: interactive sessions use the recorded final-turn
outcome (lastInteractiveTurnFinishReason), non-interactive sessions
keep the existing input.status === "completed" logic. A new
taskCompletedEmitted flag (also set by the submit_and_exit observer)
enforces exactly one task.completed per session. failSession now
records the errored final turn so a stale "completed" from an earlier
turn can never leak into the teardown emission. Telemetry only; no
user-facing behavior changes.

* chore(vscode): release v4.1.14

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on (#13498)

* fix(vscode): honor MCP auto-approve settings for SDK tool calls

The SDK extension required both the global 'Use MCP servers' auto-approve
toggle AND each tool's per-tool autoApprove flag before silently approving
an MCP call, while the legacy extension treated them as either/or. Restore
the legacy OR semantics so toggling MCP auto-approve works again.

Also key toolPolicies by the registered SDK tool name (via
defaultMcpToolNameTransform, now exported from @cline/core) instead of raw
server__tool. Servers whose names contain sanitized characters (e.g.
marketplace names like github.com/user/repo) or exceed 64 chars produced
policy keys that never matched the registered tool, so those MCP tools ran
without any approval gate; the live auto-approve lookup now re-applies the
transform instead of string-splitting the name.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Revert "fix(vscode): honor MCP auto-approve settings for SDK tool calls"

This reverts commit 86c568fbba.

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on

The SDK extension only auto-approved an MCP call when the global 'Use MCP
servers' auto-approve toggle AND that tool's per-tool autoApprove flag were
both set, so toggling MCP auto-approve appeared to do nothing and users had
to opt in each tool individually. The toggle alone now governs all MCP
tools; the per-tool flag is no longer consulted.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): release v4.1.15

* fix(cli): remove the $4.99 ClinePass promo copy (#13514)

The $4.99 first-month promo is ending, so the CLI's first-launch "Try ClinePass" dialog should no longer advertise it. Also drops the leftover CLI_PROMO_CODE plumbing, which has been an empty string since the promo-code flow was removed.

* fix(vscode): resolve hook workspace identity from the window, not shared global state (#13352)

* fix(vscode): resolve hook workspace identity from the window, not shared global state

Hook discovery, hook cwd selection, and the workspaceRoots metadata passed
to hook scripts all read the workspaceRoots/primaryRootIndex global state
keys. Global state lives in ~/.cline and is shared by every Cline instance
(all VS Code windows, the CLI, the JetBrains plugin), and nothing writes
these keys anymore, so hooks resolved against whatever project some other
or older instance last recorded. With a second window open on another
project, a workspace's .clinerules/hooks scripts were never discovered.

Resolve workspace roots via a single guarded helper backed by
HostProvider.workspace.getWorkspacePaths() (in-process, window-scoped,
same as refreshHooks): blank paths are filtered, a host-bridge failure
degrades to no workspace roots instead of silently disabling global hooks
or skipping blocking PreToolUse guards, and one resolution is threaded
through hooks-dir discovery, cache misses, cwd selection, and hook input
metadata so they can't disagree (previously up to four host lookups per
hook execution — real gRPC round trips in the standalone host). Roots and
hooks dirs are matched on whole path segments with the longest root
winning, so prefix-sharing or nested workspace roots resolve to the right
project. The adapter creates the runner once per event and skips no-op
runners, making creation the single resolution point; the separate
hasHook/getHookInfo checks are removed. The dead workspaceRoots and
primaryRootIndex state keys are dropped, and the four hand-rolled
HostProvider.workspace test stubs are consolidated into one shared
helper.

* test(vscode): add e2e coverage for workspace-scoped hook discovery

Boots real VS Code with the packaged extension against the workspace
fixture, sends a prompt, and asserts the fixture's UserPromptSubmit hook
was discovered from the open window's workspace, executed with that
workspace root as its cwd, and received the same root in its
workspaceRoots input — the end-to-end contract the hook workspace
identity fix establishes.

* test(vscode): isolate the e2e hook fixture from the shared workspace

The UserPromptSubmit fixture hook lived in the shared e2e workspace, so
every prompt-sending spec executed it (hooksEnabled defaults to true) —
and its cold PowerShell spawn on Windows pushed chat.test.ts past the
5s expect timeout. hooks.test.ts now overrides workspaceDir to a
dedicated workspace-hooks fixture, so only the hooks spec pays the hook
spawn.

* fix(hub): cap hub-events db size so it can't fill the disk (#13516)

* fix(hub): cap hub-events db size so it can't fill the disk

Row/time retention alone didn't bound disk usage: envelopes carrying
full session snapshots reach hundreds of KB each, so retained rows
could total tens of GB, sweeps only ran hourly, and DELETE never
shrinks a SQLite file. Enforce a 64 MiB size budget in prune() (oldest
rows first, VACUUM to return the space), and also prune after every
16 MiB appended so bursts can't outrun the hourly timer.

Fixes #13505

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): tolerate VACUUM failure on a full disk

VACUUM needs scratch space and can fail in exactly the state a
ballooned event log causes. The byte-budget deletes already bound live
data, so swallow the error and let the next sweep retry the reclaim
instead of aborting startup pruning and disabling the durable log.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): count the size budget in UTF-8 bytes, not characters

envelopeJson.length (UTF-16 units) and SQLite LENGTH() (characters)
undercount multibyte text by up to 3x, which could leave a CJK-heavy
log settled above budget and re-running VACUUM every sweep. Use
Buffer.byteLength and LENGTH(CAST(... AS BLOB)) instead.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(sdk): carry root overrides into the Node smoke-test sandbox (#13517)

ci-node-smoke.ts installs the packed SDK tarballs with a plain npm
install in a fresh temp dir, where the repo root package.json overrides
do not apply. When @sap-cloud-sdk 4.9.0 shipped (2026-08-24) it broke
@sap-ai-sdk/ai-api 2.14.0 (via @jerome-benoit/sap-ai-provider in
@cline/llms) with ERR_PACKAGE_PATH_NOT_EXPORTED, failing the smoke step
on every PR even though the root already pins @sap-cloud-sdk/* to 4.6.0.

Copy the root overrides block into the generated sandbox package.json
so the smoke install resolves the same pinned versions as the repo and
future third-party releases cannot break it independently.

* chore(sdk): release v0.0.79

* fix(vscode): don't steal last-used provider from ClinePass on credential refresh (#13520)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): flush the /shutdown 202 before daemon teardown

The /shutdown handler queued teardown on a microtask, which runs before
the event loop's write phase, so the daemon could process.exit() before
the accepted 202 was handed to the socket. Unix masked it (uv_try_write
lands small loopback writes synchronously); Windows has no such fast
path and lost the race regularly — the recurring shutdown.e2e.test.ts
'socket hang up' failures on windows-latest. Start teardown from the
response's write callback instead, with an idempotent 1s fallback so a
client that vanishes mid-write cannot strand the daemon, and send
Connection: close so the client gets a FIN rather than an abort.

Since the flakiness this compensated for is fixed at the source, restore
maxWorkers: 2 for the Windows core suite (serializing it cost ~3 min of
CI per run), and raise the e2e daemon discovery hang guard 10s→30s —
it guards against hangs, not runner speed.

* chore(cli): release v3.0.58

* fix(core): prevent search_codebase from crashing the process on giant single-line files (#13525)

* fix(core): prevent search_codebase from crashing the process on giant single-line files

searchWithRipgrep buffered all of rg's --json stdout into one string. Each
JSON event embeds the full text of the matched line (--max-columns is
ignored in JSON mode), so searching a directory of serialized trace dumps
(single-line multi-hundred-MB JSON files) accumulated gigabytes of stdout
until string concatenation threw RangeError: Out of memory inside the
stream data handler. That throw is outside the tool's try/catch, so it
escalated to an uncaughtException and killed the CLI/hub daemon.

Parse rg's JSON events incrementally line by line, drop events larger
than 256KB, truncate matched/context lines to MAX_LINE_CHARS, and stop
reading once maxResults is reached. The fallback regex scan now skips
files larger than 10MB (reporting the skip count) and truncates its
context lines the same way.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* simplify search_codebase crash fix to a minimal diff

Replace the incremental JSON-event parser with three small guards: stop
buffering rg stdout past 10MB, drop the trailing partial event before
parsing, and slice fallback context lines to MAX_LINE_CHARS. Drops the
fallback file-size skip and skip-count reporting.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag (#13522)

* chore(vscode): remove per-tool MCP auto-approve checkboxes from webview

MCP auto-approval is now governed solely by the global 'Use MCP servers'
toggle; the SDK approval path (shared with the CLI and desktop app) has no
per-tool granularity, so the per-tool and 'Auto-approve all tools'
checkboxes were no-ops that implied control that no longer exists. Remove
them from the MCP settings view and chat tool rows. The autoApprove arrays
in cline_mcp_settings.json and the toggleToolAutoApprove RPC are left
intact for the legacy extension.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag

Keep the checkbox components, handlers, and RPC plumbing intact but gate
rendering behind SHOW_MCP_PER_TOOL_AUTO_APPROVE=false: the SDK approval
path (shared with the CLI and desktop app) is all-or-nothing via the
global 'Use MCP servers' toggle, so the per-tool checkboxes were no-ops.
Flip the flag back on if the SDK gains per-tool approval granularity.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(tools): create new files with the platform-native line ending (#13521)

* fix(tools): use platform-native EOL for new files and preserve CRLF in apply_patch updates

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* simplify to the minimal new-file EOL fix

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* extract shared normalizeNewFileLineEndings helper

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add suggested schedule templates to the desktop Schedules page (#13529)

* Add suggested schedule templates to desktop Schedules page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix unreadable selected text in inputs caused by selection utility conflict

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Restyle Suggested section label as small gray uppercase

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Hide suggested schedule cards that match an existing schedule name

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Disable the agent todo tool and hide the Agenda UI in the desktop app (#13530)

* remove todo tool and Agenda UI, keep schedule-only tasks tool

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: biome formatting fixes

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* restore agenda backend; disable todo kind behind a flag instead of deleting

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* keep agenda automation pump idle while the todo tool is disabled

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* remove todo tool and Agenda UI altogether (revert the disable-flag hybrid)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* restore all agenda code to main state

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* disable agent todo tool and hide Agenda UI behind flags

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add Desktop App and Cloud Platform to bug report issue template (#13532)

* Add Desktop App and Cloud Platform to bug report surfaces

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename Surface Diagnostics field to Diagnostics

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: sidebar navigation cleanup with New/Schedule/Customize rows and dialog-based search (#13533)

* desktop: clean up sidebar navigation chrome

- Give New Task its own full-width labeled row below the logo row
  instead of an ambiguous icon next to the agenda toggle
- Wire the New Task row to the home action so starting a new task
  clearly takes you home (the logo still works as a fallback)
- Swap back/forward chevrons for browser-style arrow icons and
  bump their size

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: sidebar New/Schedule/Customize rows and always-visible search

- Stack New (plus icon), Schedule, and Customize as full-width labeled
  rows below the logo; whole row highlights on hover via sidebarItem
- New starts a fresh task (home), Schedule opens Settings > Schedules,
  Customize opens the Customizations sections (Plugins first)
- Show the session search bar permanently above the sessions list
  instead of hiding it behind a search icon toggle

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: move session search into a dialog behind a logo-row icon

- Replace the inline sidebar search bar with a search icon in the
  logo row that opens a cmdk command dialog listing sessions
- Selecting a result opens that session and closes the dialog
- Remove the agenda/tasks toggle the icon replaces, along with the
  now-unreachable sidebar Agenda panel (the welcome screen still
  surfaces agenda tasks)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: load full session history when the search dialog opens

Addresses Greptile review on #13533: the dialog only searched the
currently loaded history batch, so older unloaded sessions could not
be found. Opening search now kicks off loadAllSessions() (the hook's
purpose-built global-search loader), and the empty state reads
'Searching older sessions...' while more history is streaming in.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Hide Channels and Agents sections from desktop app sidebar (#13527)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop app: organize sidebar sessions into Pinned, Scheduled, and Tasks sections (#13528)

* Add Pinned/Scheduled/Tasks categories to desktop app sidebar

Replace the Schedules and Favorites filter-menu options with visible
collapsible category sections in the session sidebar, and rename the
Favorite action to Pin across the sidebar and sessions view.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Grow full history window when Tasks show-more outpaces loaded tasks

loadMoreSessions treats its argument as a limit on all sessions, but the
Tasks show-more count only tracks Task rows, so once pinned/scheduled
rows pushed the loaded total past the requested count the call no-oped
and clicks went dead. Grow the whole history window via
loadOlderSessions instead, and only when the loaded tasks cannot fill
the next page. Addresses Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Auto-fill the Tasks page instead of fetching once per show-more click

A single 50-session window growth can consist entirely of pinned or
scheduled sessions, leaving a show-more click with no visible Tasks
progress. Replace the one-shot fetch with a page-fill effect that keeps
growing the history window until the requested Tasks page fills or
history runs out. Addresses the follow-up Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Halt page-fill retries after a failed history fetch

A failed fetch leaves the task count and has-more state unchanged,
which are exactly the conditions the page-fill effect fires on, so one
failing request would retry and re-toast forever. Halt the effect after
a failure and let the next explicit show-more click retry. Addresses
the third Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Redesign desktop Model Providers page and split voice input into its own settings page (#13531)

* Redesign desktop Model Providers page and split voice input into its own settings page

- Group providers into Connected / Popular / All with auth-kind hints and
  connection status instead of per-row enable toggles
- Show browser sign-in (not an API key field) for OAuth providers, with a
  collapsed manual-key escape hatch where supported, plus explicit
  Connect / Disconnect / Sign out actions
- Move voice input to a dedicated Settings > Voice page that only offers
  connected transcription-capable providers, preselects a default model
  (streaming preferred), and stays disabled in the sidebar until a
  provider is connected

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Show native tooltip on the disabled Voice settings nav item

Disabled buttons drop pointer events, so the 'connect a model provider'
hint moves to a wrapping span for the browser tooltip to render.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Drop letter avatars and gray provider ids from provider rows and voice chips

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Drop model counts from provider list rows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename provider Connected status to Configured and drop the green styling

A settings entry is configuration, not a live connection; neutral gray
text avoids implying an active link, since the user still picks which
configured provider to use per chat.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Resync provider catalog from disk when a settings save fails

Connect/disconnect/credential edits update the list optimistically; a
failed save now reloads the catalog instead of leaving the optimistic
state (and the view's module cache) claiming a configuration that was
never persisted.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename oauthProvider test fixture to dodge CodeQL name heuristic

CodeQL's clear-text-storage query flags any identifier matching 'oauth'
as a credential source and traced the fixture's provider id into the
favorite-models localStorage write, which stores only provider/model id
strings. Renaming the fixture removes the false-positive source.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Guard catalog reloads against races and resync detail drafts on failed saves

Optimistic provider mutations now bump a generation that discards any
in-flight catalog response, so a failed-save recovery reload can't
overwrite a newer edit with an older disk snapshot. The recovery also
remounts the provider detail panel via a reset token so its local field
drafts reflect the reloaded on-disk state instead of unpersisted edits
or an optimistically cleared disconnect.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix failed-save recovery ordering and retry superseded reloads

Remount the provider detail only after the authoritative catalog reload
lands, so its drafts re-seed from disk state rather than the optimistic
values that failed to persist. When a concurrent edit supersedes the
recovery's in-flight response, retry the reload (bounded) instead of
dropping it, since that edit performs no reload of its own.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): Customize hub, sidebar overhaul, and settings polish (#13538)

* feat(desktop): merge customization pages into a Customize hub with inline marketplace

Replaces the Plugins page and the dedicated Marketplace page with a single
Customize hub. Tabs: Skills, MCP, Plugins, Rules, Hooks, Tools, each with
live counts. Tabs backed by a marketplace catalog render the installed
items followed by an inline browsable Browse section (CLI-hub style), so
installing from the catalog immediately reflects in Installed above.

- Installed cards restyled to mirror the browse-card anatomy: bg-card p-4
  containers, absolute top-right xs Uninstall matching Install, truncating
  semibold titles, primary-tinted icons, real Badge components instead of
  ad-hoc bordered spans, un-indented line-clamped descriptions
- Rules/Hooks/Tools rows brought into the same card language; redundant
  intro paragraphs (duplicating the page description) removed; Tools group
  headers match the Installed header style with counts
- Marketplace section header renamed to Browse; duplicate 'N results' row
  removed (the header count is the single source)
- MCP embedded view now shows the full marketplace instead of
  installed-only

* feat(desktop): overhaul sidebar sessions and navigation

Sessions list:
- Sort toggle removed; sessions are always grouped by project, with pinned
  sessions leading each group (both subsets ordered by recency). The
  Pinned/Scheduled/Tasks category sections and their time-mode paging
  machinery (page-fill effect included) are deleted
- Scheduled sessions get an inline clock icon next to the pin position;
  pin + clock render together when both apply, and the running/unread
  status dot now coexists with them
- One font size (text-sm) across the list: titles, timestamps, project
  headers, show-more buttons, empty states. sidebarText needed !text-sm
  because the default button size's text-base wins the twMerge conflict
- Gradient fade under the Sessions header once the list scrolls, so rows
  fade out instead of hard-clipping
- The session-detail hover card is controlled from the sidebar and closes
  on scroll (Radix receives no pointer events while scrolling, so it used
  to float over moving content)
- Sidebar min resize width raised 224->260px; the per-project show-more
  label truncates so its nowrap text can't force rows to overflow and clip
  timestamps at narrow widths

Navigation:
- Customize replaces the Plugins/Marketplace/Hooks/Rules/Tools sidebar
  entries; Schedules and Customize are hidden from the expanded settings
  nav (their top rows cover them) but stay reachable when collapsed
- The settings gear always opens General instead of resuming the last
  section; the Account no-op hover special case is gone
- The New row highlights (aria-current) while the fresh not-yet-started
  task page is showing and hands off to the session row once the task
  starts; hitting New also focuses the prompt input via a window-event
  signal (lib/prompt-input-focus.ts) since the sidebar and composer sit in
  distant subtrees
- Fixed the xs button size collapsing any icon-bearing button to 12x12
  (leftover has-[>svg]:size-3 from when xs was a micro button) — this was
  why Uninstall buttons rendered broken next to Install

* feat(desktop): polish settings pages and chat composer

Models page:
- The provider detail panel is always open: no X button, no empty
  no-selection state. It defaults to the first connected provider (falling
  back to the first in the catalog), which also removes the layout shift
  that happened when the page swapped between full-width and panel
  variants on selection
- Fixed the list pane becoming unscrollable while the panel was open:
  grid items default to min-size auto, so the pane grew past its track
  inside the overflow-hidden grid and its ScrollArea had nothing to
  scroll; wrapped it in a min-h-0 min-w-0 cell
- Add Provider opens a Dialog instead of swapping the page
  (AddProviderContent gained a dialog variant that renders only the form)
- Embedded inputs (provider search, model search, detail fields) share one
  EMBEDDED_INPUT_CLASS stripping the Input component's own border/dark bg
  tint/shadow/ring, which rendered as a mismatched inner box; the model
  search box uses the same h-9/px-3 frame as the provider search
- Model list flows with the page instead of a max-h capped inner scroller

Other pages:
- Account uses the shared PageFrame/PageHeader: left-aligned, text-3xl
  title, Sign Out in the header actions slot
- Desktop notifications is one General section: header row plus the
  Event/Notify/Sound matrix nested in a card, so its rows no longer read
  as top-level peers of Dark mode; 'Available in the desktop app' label
  removed
- Schedule page retitled from Schedules with a real description; Customize
  description rewritten

Chat composer:
- The voice dictation button only renders once a voice model is
  configured (Settings -> Voice); the unconfigured deep-link state is
  gone (prop type kept for an easy restore)

* chore(desktop): release v0.0.17

* fix(desktop): unblock sdk-test lint on the voice-input model picker (#13553)

The model picker renders a radiogroup of styled buttons with role=radio
and aria-checked; biome's useSemanticElements flags the role as an
error, which fails the sdk-test Quality Checks lint for every PR
touching sdk/ or apps/ paths. Suppress with a justification — switching
to input type=radio needs a restyle and belongs to the desktop settings
work.

* fix(vscode): include rich workspace metadata in system prompt (#13518)

* capture richer workspace information for vs code extension

* fix(shared): redact credentials from workspace remotes

* fix(shared): avoid regex backtracking in remote redaction

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>

* Hide task costs on vscode when ClinePass is selected (#13515)

* fix: stop showing cost estimates for subscription-billed providers (#13552)

* fix(vscode): stop showing cost estimates for subscription-billed providers

Providers whose usage is covered by a flat-rate subscription (ChatGPT
Plus/Pro via openai-codex, ClinePass) are marked with
metadata.usageCostDisplay = "subscription" in the SDK, and the CLI
already suppresses dollar figures for them. The VS Code host collapsed
that value into "show" before it reached the webview, so the task
header and model pricing rows rendered API-rate cost estimates that
users read as real charges on top of their subscription.

Pass all three usageCostDisplay values ("show" | "hide" |
"subscription") through the catalog listing and render cost only when
the value is "show", matching the CLI's shouldShowCliUsageCost
policy.

* feat(llms): mark Claude Code as a subscription-billed provider

Claude Code is typically authenticated with a Claude Pro/Max
subscription, but its models reuse Anthropic API pricing metadata, so
Cline rendered per-token prices and API-rate cost estimates for usage
that is covered by the subscription. Set usageCostDisplay =
"subscription" on the provider (picked up by the CLI and the VS Code
webview) and suppress the price rows in the Claude Code settings card.

The Claude Code CLI can also run on API-key billing, where a real cost
exists; the provider cannot distinguish the two, so we prefer showing
no number over a misleading one.

* fix(vscode): suppress cost display until provider listings load

While the ListProviders request is in flight (or after it fails), the
usage-cost hook had no listing to consult and fell back to "show",
flashing the API-rate estimate at subscription users on every chat-view
mount — the exact display the previous commit removes. Return
"unknown" whenever listings are absent; consumers already render cost
only for "show", so they suppress it during that window with no
changes. Briefly hiding a real cost is harmless, briefly showing a fake
charge is not.

* fix(desktop): reconcile voice settings after main sync

* test(llms): allow experimental ElevenLabs models

* fix(sdk): preserve canonical media model behavior

* feat(desktop): customize macOS DMG install window (#13563)

* feat(desktop): add Retina DMG background tooling

* feat(desktop): customize the macOS DMG layout

* ci(desktop): validate DMG background assets

* fix(desktop): adjust DMG Applications icon position

* ci(desktop): drop redundant DMG artwork validation from publish workflow

Tauri's beforeBuildCommand already runs dmg:background (with its own
validation) at the start of the build/sign/notarize step, and the
release/beta config overlays do not override the build section, so this
step duplicated work the publish job performs anyway. PR-time coverage
lives in desktop-test.yml.

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>

* feat(desktop): sidebar time view, Customize/Marketplace split, and schedule page UX (#13570)

* feat(desktop): split Customize into Installed and Marketplace pages

The Customize hub previously embedded a Browse section inside every tab
that had a catalog. That inlining made each tab long and buried the
catalog. Customize is now the installed inventory only (skills, MCP,
plugins, rules, hooks, tools tabs pass marketplaceVariant="installed"
to the embedded MarketplaceView; McpServersContent grew the same prop),
with an outline Marketplace button in the header.

Browsing moved to a dedicated Marketplace settings section that renders
the previously dead "directory" variant of MarketplaceView: one list
across all catalog types with type-filter chips, wrapping tag chips,
and light rules separating the filter tiers from each other and from
the results. The Clear control now renders inline at the end of the tag
row only while a tag is active, the Updated date is gone, and the
header hosts an Installed button mirroring the one on the Customize
page. Directory subheader copy: "A curated set of plugins, MCP
servers, and skills from the Cline community."

Tag and type chips wrap to new lines instead of scrolling
horizontally.

* feat(desktop): sidebar time view with sections, sort toggle, and scheduled detection

Restores the time-sorted session list as the default sidebar view, with
collapsible Pinned / Scheduled / Tasks sections (headers appear only
once something is pinned or scheduled) and the page-fill effect that
grows the fetched history window until a Show-more click makes visible
progress. Project grouping stays as the alternate mode behind a
one-click sort toggle whose icon reflects the active mode — the old
dropdown cost an extra click for a two-option choice.

Scheduled sessions are detected two ways: the hub-schedule origin
trigger in session metadata, plus a fallback that asks the hub which
session ids belong to schedule executions (list_routine_schedules,
fetched on mount and every two minutes, merged into a rolling set).
The fallback matters because locally executed scheduled runs do not
reliably stamp the trigger into session metadata — a real scheduled
session created today carried only {mode:"user"} provenance. The
scheduled clock icon now leads the row, left of the title; pin and
timestamp stay on the right.

The initial visible page grows from 10 to 30 rows so a tall sidebar
fills instead of stranding a stub of rows over empty space (history
fetches already start at 50).

The expanded sidebar's Customize row now hosts indented Installed and
Marketplace sub-tabs while a customize section is open; the active
sub-tab carries the full selected background while the parent keeps a
subtler one so the two simultaneous highlights read differently.

Also fixes the hover-card flash on click (logo card and session-row
cards): Radix HoverCardContent sits on a DismissableLayer, so a click
on the trigger registers as a pointer-down outside the card and
dismisses it, and the trigger's focus event immediately reopens it.
onPointerDownOutside preventDefault suppresses the dismissal; cards
still close on pointer leave.

* feat(desktop): schedule page row, dialog, and details UX polish

Schedule cards are now click targets: clicking anywhere on a card
outside its controls opens the details dialog (guarded via
closest("button,...") since every inline control, including the Radix
switch, renders a button element), with Enter/Space keyboard support.
The redundant eye button is gone. The remaining edit / run / pause /
delete buttons grow from the 12px icon-sm size to 28px targets with
16px icons, sized consistently with the adjacent enable toggle — the
icons use explicit size-4 classes so the Button base svg rule cannot
shrink them back.

The new/edit dialog gains breathing room between field labels and their
inputs (space-y-2 per field wrapper).

The details dialog no longer scrolls as a whole when the schedule JSON
is long: the dialog is a flex column capped at 85vh, the JSON pre
shrinks to the remaining space (min-h-0) and scrolls internally, and
the Runs tab list scrolls inside the tab the same way.

* feat(desktop): scheduled sessions UX — unified details dialog, run-now handoff, hidden steering, stuck-thinking fix (#13573)

* feat(desktop): merge schedule details into one view and open run-now sessions

The schedule details dialog drops its Overview/Runs tabs: one scrollable
column with the meta grid, the configuration JSON (capped at max-h-64
with internal scroll so it cannot crowd out what follows), and a Runs
section beneath it showing the three most recent runs with a ghost
"Show all N runs" expander (collapsed again whenever a different
schedule's details open). The "Full configuration for this schedule"
subtext is gone; the dialog passes aria-describedby={undefined} so
Radix does not warn about the missing description.

Run now hands you into the session it starts. The trigger command
queues the run and returns before the runner attaches a session id, so
after the toast the handler polls the schedule overview once a second
for up to 15 seconds — which doubles as keeping the page's run status
fresh (refreshSchedules now returns the fetched overview to make that
single-stream) — until the triggered execution reports its session id,
then calls onOpenSession. Guarded so it never auto-navigates after the
user left the page.

* feat(desktop): hide runtime steering messages from transcripts

Scheduled/automation runs inject user-role steering messages each
iteration ("[SYSTEM] This run is not complete until you call
submit_and_exit...", plus a team-obligations variant). The chat view
rendered them as user bubbles, as if the person had typed them — in a
scheduled session the transcript was mostly [SYSTEM] noise.

They are machinery talking to the model, not something the person said
or needs to read, so the transcript now hides them entirely:
MessageBubble renders null for any [SYSTEM]-prefixed user message.
Grouping still treats them as working-row machinery via a single
isSystemSteeringMessage predicate — they collapse into the run's work
span, are never a turn boundary, can never be mistaken for a run's
answer, and never advance the run count even when metadata is missing —
so work-block folding and checkpoint/edit run numbering stay correct.
A finished scheduled session now reads as prompt, work summary, answer.

* fix(desktop): poll history while an attached session's event stream is dead

Opening a scheduled session while (or right after) it runs left the
view stuck on the thinking shimmer until the user switched away and
back. Root cause is in core: the hub daemon executes scheduled runs on
a private LocalRuntimeHost inside createLocalHubScheduleRuntimeHandlers,
while the hub server only projects live events from its own session
host — so session.attach succeeds but no assistant/tool/status events
ever flow. And since multiple hub daemons share cron.db, a run claimed
by a different daemon is invisible to this hub regardless. The proper
core rewiring is tracked as ENG-2474.

Client-side heal that covers every case: while an attached history
session reports a busy status and no chat_event chunk has arrived for
five seconds (and no assistant bubble is mid-stream), poll every three
seconds — re-read canonical history, merged through the same dedupe
path hydration uses, and the session record's status — so the
transcript and the thinking indicator settle in place. Locally driven
turns keep chunks flowing, so the quiet-window guard keeps the fallback
inert there.

* chore(desktop): format workspace selector components

Biome formatting drift that landed on main; picked up by a formatter
pass over components/views/chat.

* fix(desktop): keep stale-stream poll inert during locally driven turns

The fallback poll could fire between a local submit and the model's
first chunk (optimistic user bubble added, stream quiet past the
window, no assistant bubble yet). It then replaced the optimistic
bubble — raw prompt text — with its canonical history twin, which is
stored wrapped in a user_input envelope. The rekey handler that runs
when the stream starts looks for a trailing user bubble matching the
raw prompt, finds only the wrapped copy, and appends a second bubble:
duplicated messages in normal interactive chat.

The poll now stays inert while a local turn is in flight
(turnEpoch !== turnSettledEpoch, or outstanding optimistic user
messages), checked both before polling and again after the snapshot
returns. Hydration marks the turn settled — the mount defaults
(epoch 0, settled -1) otherwise read as an open turn and would keep
the fallback inert forever for the scheduled-session case it exists
for. Applying a polled snapshot also rebuilds the live tool routing
keys, same as hydration, so later tool events update canonical rows
in place instead of appending.

* fix(desktop): keep the working indicator alive for narrating scheduled runs

Watching a scheduled run live: the first tool row appeared, then the
thinking indicator vanished with nothing streaming, and the rest of
the run (final answer, submit_and_exit) only showed up seconds later
in one lump.

inferHydratedChatStatus treats a "running" session record whose
transcript ends on an assistant message as a session that died without
a status flip and reports "completed". That heuristic is right for
stale records, but scheduled/automation models narrate between tool
calls, so a polled snapshot can genuinely end on assistant text
mid-run — the completed flip hid the working indicator, folded the
run early, and disarmed the stale-stream poll (status left the busy
set), dead-ending live updates until an in-flight poll happened to
deliver the finished run.

The heuristic now only applies once the transcript has actually gone
quiet (newest message older than two minutes — comfortably past model
latency plus tool runs). A recently active transcript keeps the
record's "running" verdict, so the indicator stays up and polling
stays armed until the record itself settles.

* fix(desktop): stale-stream poll mirrors the session record instead of inferring

Replaces the previous fix for the vanishing working indicator (the
time-window guard added to inferHydratedChatStatus) with a version
that adds no inference at all: the heuristic is restored to exactly
its long-standing form, and the poll now maps the session record's
status verbatim (mapSessionRecordStatus).

The record is the right authority in the poll's context: the sessions
this fallback serves have a live host maintaining their record, and it
flips to a terminal status when the run ends. Transcript-shape
inference belongs only where it has always lived — hydrating sessions
whose records may be orphaned — and would misread a mid-run snapshot
ending on assistant narration as a finished session, hiding the
working indicator and disarming the poll.

* fix(desktop): address review findings on steering detection and run-now matching

Steering detection additionally requires the injected-message marker
(meta.userRunSpan === 0) beside the [SYSTEM] prefix, so a person's
genuine prompt that happens to start with "[SYSTEM]" stays visible
and turn-counted. The failure direction is deliberate: an unstamped
injected reminder would merely show as a user bubble, while the
content-only check could hide a real prompt.

Run-now only follows the execution id the trigger reply itself named;
the newest-execution-for-this-schedule fallback could open a previous
run's session when the trigger failed to enqueue one.

* fix(desktop): report a failed run-now instead of confirming a start

A trigger reply without an execution means no run was enqueued (the
schedule may have been disabled or deleted since the page loaded). The
handler previously toasted "Run started" regardless and then silently
skipped the session-open polling. It now shows a destructive
"Run not started" toast, refreshes the schedule list so the row
reflects reality, and skips the polling entirely.

* fix(desktop): don't block the main thread on quit while stopping the sidecar (#13566)

Quitting the mac app beach-balled for ~5-7s. The shutdown POST was
built from the ws transport URL (appending /shutdown lands inside the
query string), so the sidecar was never told to exit, and stop() then
polled the child for up to 7s on the main thread - on macOS inside
applicationWillTerminate - before SIGKILLing it.

stop() now sends SIGTERM and returns immediately. The sidecar handles
SIGTERM with the same bounded (5s) graceful shutdown as the /shutdown
endpoint and exits itself, finishing session persistence as an orphan.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): hover trash button on sidebar session rows (#13582)

Each session row shows a trash icon on the right while hovered (or
when the button itself is focused), opening the same delete
confirmation dialog the row's context menu uses. The row is a button
and buttons cannot nest, so the trash is an absolutely positioned
sibling inside a group/row wrapper, overlaid where the timestamp sits:
row hover hides the timestamp, shows the trash, and moves the row's
hover background to the wrapper group so it holds while the pointer is
on the trash itself.

* fix(desktop): install marketplace plugins and MCP servers in-process instead of spawning a cline binary (#13585)

* fix: install marketplace plugins and MCP servers in-process instead of spawning a cline binary

The desktop app sidecar and cline-hub shelled out to 'cline plugin install'
and 'cline mcp install' for marketplace installs. Packaged GUI apps inherit
launchd's minimal PATH on macOS and most desktop users have no cline CLI
installed at all, so installs failed with a red
'Executable not found in $PATH: "cline"' error.

Install via @cline/core's installPlugin/installMcpServer in-process instead,
matching what the VS Code extension already does. Also fix
parseMcpInstallArgs in @cline/core to treat the marketplace catalog's '--'
separator as end-of-options; previously the separator itself became the
stdio command.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop test-injection plumbing from marketplace installers

Call @cline/core's installPlugin directly instead of threading an
installer option through the marketplace entry points; tests stub the
core module instead.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert: keep cline-hub marketplace installs CLI-backed

The hub dashboard is launched via 'cline dashboard', so a CLI is always
present and CLINE_WRAPPER_PATH resolves it; the PATH bug only affects
the desktop app, which does not ship a CLI.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.18

* chore(vscode): release v4.1.16

* chore(sdk): release v0.0.80

* chore(cli): release v3.0.59

* fix(hub): stop shipping full transcripts inside broadcast hub events (#13587)

* fix(hub): stop shipping full transcripts inside broadcast hub events

Every session.updated (and session.created/detached/run.started) event
embedded the session's ENTIRE message transcript via readCoreSessionSnapshot,
even though no consumer reads snapshot.messages off an event — clients fetch
messages with the session.messages command. For a multi-megabyte transcript
this turns every status flip into megabytes per subscriber, floods the
durable event log, and (until the send-queue backpressure fix lands) lets a
slow subscriber balloon the hub process by one full transcript copy per
event — reported as a 25GB cline process on a 16GB Mac.

Strip snapshot.messages centrally in HubServerTransport.publish() so every
current and future event publisher is covered, the event log stores slim
envelopes, and cursor replay stays byte-identical with live fan-out. All
other snapshot fields (status, usage, model, workspace, checkpoint) are kept,
and command replies are untouched.

* fix(hub): never capture the transcript into event/reply snapshots

Replaces the publish-boundary strip with the real fix: don't build
message-bearing snapshots in the first place. emitSessionSnapshot no longer
re-reads the entire transcript from disk on every status flip, and
readCoreSessionSnapshot no longer reads it for any event or reply — a
snapshot is a state notification (status, usage, model, workspace,
checkpoint); the transcript is fetched via the session.messages command.
Checkpoint-restore snapshots (session-versioning-service) are untouched:
restore replies carry messages in their own dedicated field.

* chore(desktop): release v0.0.19

* chore(sdk): release v0.0.81

* chore(cli): release v3.0.60

* fix(vscode): avoid render crash on malformed api_req payloads in combineApiRequests (#13560)

* fix(vscode): stop pinning DeepSeek model count in catalog smoke test (#13600)

* feat(ui): share agent welcome hero (#13567)

* feat(ui): share agent welcome hero

* test(ui): cover welcome hero pointer states

* refactor(ui): keep welcome hero API minimal

* test(ui): verify welcome hero package assets

* fix(ui): inline welcome hero masks

* fix(tools): preserve a file's own CRLF line endings across apply_patch updates (#13512)

* fix(desktop): keep the window title bar draggable across views (#13572)

* fix(desktop): keep window title bar persistent

* fix(desktop): reserve persistent title bar space

* fix(desktop): polish persistent title bar layout

* Sign Windows CLI binaries with Azure Trusted Signing; surface app-control launch errors (#13021)

* feat(cli): sign Windows binaries with Azure Trusted Signing and surface app-control launch errors

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): use _CLI-suffixed signing profile secret, normalize endpoint, fail loud on partial config

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* Tunnel ProtoBus over the existing Host Bridge (#13218)

* feat(core): tunnel ProtoBus over Host Bridge

* fix(core): harden Host Bridge stream lifecycle

* fix(core): serialize concurrent chunked responses per request

Streaming handlers deliver updates fire-and-forget, so two logical
responses for one request_id can be in flight at once. Chunked payloads
made forwarding non-atomic: each chunk write is an await, so concurrent
forwards could interleave their chunk sequences and the receiver --
which reassembles purely by arrival order -- would splice two payloads
into one. Route all forwards for a request through one promise chain; a
failed write rejects every later forward so a torn payload is never
followed by more chunks.

Rename the lock manager's instanceAddress to instanceOwner: it holds an
opaque per-spawn instance ID on the token path and a listener address
only on the CLI-harness path. Delete the caller-less getInstanceByPort
query that interpreted the owner as an address.

Also: document message_json as a legal wire encoding for small
payloads, close the gRPC client when startup fails, note the
intentional discard of the cancellation confirmation, and add the
proto's trailing newline.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* Build and Authenticode-sign a Windows x64 desktop installer in desktop releases (#13607)

* feat(desktop): build and Authenticode-sign a Windows x64 NSIS installer in desktop releases

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): pin OIDC-adjacent actions to commit SHAs in the Windows signing job

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): pin checkout and upload-artifact to commit SHAs in the Windows signing job

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): show agent-created schedules on the Schedules page (#13613)

* fix(desktop): show agent-created schedules on the Schedules page

Schedule hub commands are scoped to the workspace registered by the
connection, but the desktop app's hub client registers the app launch
directory while agent-created schedules live under each chat's own
workspace folder - so they never appeared on the Schedules page.

Grant token-authenticated hub connections (which can already bind any
workspace at registration) explicit cross-workspace schedule access via
an allWorkspaces payload flag, and have the desktop sidecar request it
for routine schedule commands. Workspace-bound clients (local browser
origins) and default CLI behavior stay scoped.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(core): strip allWorkspaces flag from schedule inputs and pin it in the sidecar payload

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make suggested routine template prompts prescriptive about their final output (#13611)

* Make bug hunter routine template prescriptive about its final report

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make remaining routine templates prescriptive about their final output

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop: surface scheduled-task final output — auto-expand submit_and_exit and render its summary as markdown (#13612)

* desktop: auto-expand submit_and_exit and render its summary as markdown

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: render submit summary in full foreground color

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: label the submit row 'Scheduled task completed'

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: label errored submit_and_exit rows as failed

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add tooltips explaining Live and After recording badges on voice input models (#13610)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Remove box shadow from chat message actions row (#13630)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): make the Tauri shell work on Windows (#13632)

- Defer updater installation to the user-initiated restart on Windows:
  install() launches the NSIS installer and exits the process immediately,
  so the background cycle now downloads only and stages the bytes, and
  restart_to_apply_update installs them after stopping the sidecar.
- Spawn child processes (sidecar, git, cmd /C start) with CREATE_NO_WINDOW
  so the GUI-subsystem app doesn't pop visible console windows.
- Fall back to USERPROFILE when HOME is unset resolving the MCP settings
  path, matching the sidecar's homedir().
- Reap the sidecar after the Windows hard-kill so its exe file lock is
  released before the NSIS installer replaces it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop watching agenda spec dirs while the todo tool is disabled (#13629)

* fix(core): stop watching agenda spec dirs while the todo tool is disabled

Since #13530 disabled the agent todo tool, the Agenda UI, and the
automation pump, the hub still created fs.watch watchers on the global
agenda specs dir and on every workspace root recorded in the task store
(at startup and on scope access). Nothing consumes the watcher-driven
task events while the feature is off, and the task.* hub commands
already reconcile spec files on demand, so the watchers are pure
overhead - one OS watch handle per known workspace.

Wire watchFiles to AGENDA_TODO_TOOL_ENABLED the same way
automationEnabled is, preserving a host's explicit watchFiles opt-out
for when the flag is turned back on. Schedules are unaffected: the
schedule list has no file watcher and updates through hub commands and
published schedule events.

* fix(core): reconcile external spec edits inside updateTask

With the spec watchers off there is no background reconciliation, so a
task spec edited directly on disk made every same-store task.update fail
the signature check with "task spec changed outside the manager" until
an unrelated task.get or task.list happened to reconcile the scope.

Reconcile the task's scope at the start of updateTask (mirroring what
refreshAndVerifyTaskIntent already does for approve/run), skipping it
when the file reconciler itself is the caller to avoid recursing from
reconcileFileStore. An external edit now surfaces as the store's normal
stale-revision conflict, and a re-read-and-retry succeeds. This also
closes the pre-existing watcher debounce race for updates.

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently (#13565)

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently

Port the cline-provider refresh semantics to openai-codex and oca:
a transient refresh failure (network error, timeout, server 5xx) with an
already-expired access token now rethrows instead of returning null.
A null return means the refresh token was REJECTED and re-auth is
required; treating an outage blip as a rejection is what turned it into
a forced 'openai-codex requires re-authentication.' task stop while the
settings UI still showed the user as signed in.

Both providers also emit user.auth_refresh_soft_failure telemetry on
transient failures (the 'prevented logout' counter the cline provider
already has) and attach status/errorCode details to the genuine
invalid_grant logout event.

* refactor: collapse duplicate soft-failure telemetry branches and test

Review feedback: compute tokenExpired once and emit the soft-failure
event once in both providers, then return current credentials or
rethrow. Fold the codex soft-failure telemetry assertions into the
existing still-usable-token test instead of a near-duplicate case.

* fix: make OpenAI Codex (ChatGPT subscription) sign-in fail loudly instead of silently dead-ending (#13537)

* fix: make OpenAI Codex sign-in fail loudly instead of silently dead-ending

When callback port 1455 is already in use (e.g. by the Codex CLI or a
previous pending sign-in), startLocalOAuthServer returns a no-op server
and loginOpenAICodex would open the browser anyway, then dead-end:
the callback could never be received, and in the VS Code extension the
user just saw nothing happen after clicking 'Sign in to OpenAI Codex'.

- loginOpenAICodex now fails fast with an actionable 'port in use'
  error before opening the browser, unless the host provides manual
  code entry (the CLI's paste fallback keeps working)
- surface OAuth redirect errors (e.g. access_denied) instead of
  collapsing them into 'Missing authorization code'
- the extension dedupes concurrent sign-in clicks: a re-click re-opens
  the auth page of the pending flow instead of spawning a second flow
  that would collide with our own callback server
- browser-open failures now show an error message with the URL to
  open manually instead of only logging
- abandoned-flow timeouts no longer surface a confusing 'Missing
  authorization code' toast

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop host-side codex login dedupe, keep flow identical to CLI

The SDK owns the failure handling now (fail-fast on an unbindable
callback port), so the extension keeps the exact same simple
loginOpenAICodex call the CLI uses. A second click while a flow is
pending gets the SDK's clear port-in-use error, same as running
'cline auth openai-codex' twice would. Keep only the CLI-parallel
onOpenUrlError surfacing (the CLI prints 'open the URL above
manually'; the extension's equivalent is an error toast with the
URL).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(e2e): cover Codex sign-in callback-port failure and redirect errors

Two driven-VS Code tests for the OpenAI Codex (ChatGPT subscription)
sign-in flow:

- with port 1455 occupied on both loopback families, clicking the
  sign-in button surfaces the fail-fast port-in-use toast
- with the port free, the callback server binds and an OAuth redirect
  error (access_denied) propagates to a visible error toast

The second test opens a real browser tab to the OpenAI auth page as a
side effect of the genuine sign-in click.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* feat(core): anchor agent-created schedules in the user's .cline schedules home (#13634)

* feat(core): anchor agent-created schedules in the user's .cline schedules home

Agent-created schedules inherited whichever workspace folder the chat
session happened to run in, scattering user-level routines across chat
and project folders. They were invisible to workspace-scoped listings
elsewhere, tied to folders that may be cleaned up, and each chat's
tasks tool saw a different set when checking for duplicates.

Anchor them in ~/.cline/schedules instead: the hub's scheduled-task
session defaults now resolve to that home (created on demand), so
agent-created schedules live and run in one stable user-level scope.
The tasks tool guidance now tells agents that scheduled sessions run in
the schedules home, so prompts must carry absolute paths to any project
they operate on.

Schedules created explicitly with a workspace (CLI --workspace, desktop
routine wizard) are unchanged, and existing rows keep their current
workspaceRoot - they stay visible through the all-workspaces listing
paths (#13613, #13633).

* test(core): restore any pre-existing CLINE_DIR after the agenda hub test

The test's cleanup deleted CLINE_DIR outright, so an environment that
had it configured would leave later tests in the same worker on the
default storage directory. Save the previous value and restore it.

* test(core): restore CLINE_DIR even when hub test setup throws early

Restoring the override in the try/finally missed failures thrown during
transport construction or start(), before the try was entered. Register
the restore with onTestFinished instead, which runs regardless of where
the test fails.

* fix(desktop): don't show providers as configured without real credentials (#13608)

* fix(desktop): don't show providers as configured without real credentials

The desktop settings marked any provider with a persisted settings entry
as Configured, but legacy VS Code migration and empty saves can seed
entries (e.g. qwen-code, sapaicore) holding only a default model and no
credentials. Move the CLI's isProviderSettingsUsable readiness check into
@cline/core, expose it as a computed 'configured' flag on the provider
catalog, and use it in the desktop's isProviderConnected.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): resync catalog after saves so Configured badge updates live

Optimistic provider mutations can't know the sidecar-computed 'configured'
flag, so after connecting a keyless provider or saving cloud credentials
(e.g. a Vertex project id) the row stayed 'Not configured' until remount.
Silently refetch the catalog after each successful save, guarded by the
existing generation counter so newer edits discard stale responses.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): claim a generation in post-save resync so overlapping refreshes can't apply stale snapshots

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): bump catalog generation on OAuth login success

Every other optimistic provider mutation claims a new generation; the
OAuth success path didn't, so a catalog load or resync still in flight
could arrive late and overwrite the just-connected state.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): resync catalog after OAuth login instead of bare generation bump

The resync claims a new generation (discarding any stale in-flight
response) and its own fetch covers both the new OAuth connection and any
provider saved moments earlier, matching the post-save path.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint (#13626)

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint

Restoring a checkpoint runs git reset --hard, which moves the current
branch pointer. If commits were made after the checkpoint (by the user
or by the agent), the reset silently knocked them off the branch,
leaving them reachable only through the reflog.

Guard the reset: if HEAD no longer matches the commit the checkpoint
was created on, throw a descriptive error (including how many commits
would be dropped) instead of destroying history. Chat-only restore is
unaffected, and users who really want to discard the commits can reset
the branch manually first.

Fixes #13550

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): close the guard-to-reset race with an atomic ref update

The moved-HEAD guard read HEAD, ran further git commands, then reset
unconditionally, so a commit landing in that window could still be
knocked off the branch. Replace the reset's branch move with git's
native compare-and-swap (git update-ref HEAD <new> <old>), which fails
if HEAD no longer points at the verified commit, and follow with a bare
reset --hard to sync the index and worktree to the already-moved HEAD.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: hide history cost estimates for subscription-billed tasks (#13562)

* fix(vscode): hide history cost estimates for subscription-billed tasks

The task-header fix for subscription providers cannot reach history:
history rows render the stored totalCost (an API-rate estimate) and do
not know which provider ran the task, so the history page printed
$X.XXXX on every row and the recent-task chips in an empty chat view
rendered a $ chip even for subscription-billed tasks.

The SDK session records already persist the provider — the CLI's
history view uses it for exactly this — but the VS Code mappers dropped
it. Map it through both transports (HistoryItem.apiProvider for the
state-pushed taskHistory, TaskItem.api_provider for getTaskHistory) and
suppress the dollar figure per row when that provider's
usageCostDisplay is not "show", via a new useUsageCostVisibility
predicate shared by both surfaces.

Rows without a recorded provider (tasks predating the field, legacy
imports) keep showing the stored value — there is nothing to key
suppression on.

* test(vscode): e2e-verify history cost suppression in real VS Code

Seeds SDK session records (one openai-codex subscription task, one
anthropic usage-billed task) into the isolated CLINE_DIR before the
webview loads, then asserts in a real VS Code instance that both the
recent-task chips and the full history page render the dollar figure
only for the usage-billed task. Covers the two boundaries the unit
tests stub: on-disk records reaching getTaskHistory with provider
populated, and the provider listings delivering the subscription mark
to the webview.

* Fix scheduled tasks disappearing after desktop app updates (#13627)

* Fix hub-managed schedules being wiped by cron reconciliation on hub restart

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Require the virtual hub/schedules path when exempting specs from removal reconciliation

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Treat recorded source mtime as proof a spec is file-backed, closing the hub/schedules spoof gap

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(shared): discover global rules at ~/Cline/Rules (#13614)

The VS Code Rules tab resolves the Documents folder via
'xdg-user-dir DOCUMENTS', which prints bare $HOME when no user-dirs
config exists (WSL/headless installs), so it reads and writes global
rules at ~/Cline/Rules. The SDK's rule search paths only covered
~/Documents/Cline/Rules, so those rules never reached the system prompt.
Add the missing path to the search list.

Fixes #13542

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat: add searchable session history (#13420)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* Fix CLI crash when a remote MCP server is offline but enabled (#13639)

Remote (SSE/streamable HTTP) MCP connects run on the session.create
critical path, which the hub caps at 30s. Without a connect budget an
unreachable server spent the full 60s default request timeout (with the
SSE transport stuck in a reconnect loop), stalling session.create past
the hub deadline and tearing the whole session down - the interactive
TUI exited and one-shot runs failed. Stdio servers already have a
bounded initialize budget for exactly this reason; give URL clients the
same treatment with a 10s default connect budget that an explicit
timeout overrides in either direction.

Fixes #13597

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(vscode): prevent E2E worker teardown hangs (#13644)

* test(vscode): capture external URLs in E2E runs

* docs(test): clarify browser capture rationale

* Add a GitHub integration step to the onboarding (#13225)

* Add feature flags to the app

* React to account updates

* Address comments

* Add a GitHub integration step to the onboarding

* validate domain and fix errors on auth

* Hide the step behind a feature flag

* update version

---------

Co-authored-by: John Choi <john.choi@cline.bot>

* fix(ci): stop e2e worker teardown timeouts and deflake hub daemon e2e on Windows (#13646)

* fix(e2e): stop VS Code e2e worker teardown from timing out

The ext-vscode-test-e2e job has been failing on main with 'Worker teardown
timeout of 60000ms exceeded' even though every test passes. Playwright only
reports an Electron app as closed once the process exits AND every holder of
its stdio pipes is gone (ChildProcess 'close' waits on the extra fd3/fd4
pipes Playwright creates for Electron). Any VS Code descendant that outlives
the main process (chrome_crashpad_handler, GLib's 'dconf watch' helper,
xdg-open browser handlers, VS Code 1.135's agent host CLI subprocess that
logs 'unable to kill the process') keeps those pipes open, so app.close()
never resolves and the worker teardown hangs on it until its 60s timeout
fails the job.

Harness fixes, each removing one source of that wedge:

- closeAppForTeardown now SIGKILLs the whole process group (taskkill /T on
  Windows) when app.close() times out, instead of only the main pid — and
  does so even when the main process already exited, which is exactly the
  wedged state. Playwright launches Electron detached, so pid == pgid.
- Launch VS Code with --disable-crash-reporter so no crashpad handler
  outlives the app holding the harness pipes.
- Seed the fresh user-data-dir with chat.disableAIFeatures: true so VS
  Code's own AI features (rolled out via server-side experiments, so CI
  breaks without any repo change) never start their agent host process.
- Drop the page.close() teardown: closing VS Code's last window quits the
  whole app, and ElectronApplication.close() on an already-exited app
  deadlocks; the app fixture's app.close() closes windows itself while the
  app is alive.
- Codex sign-in no longer opens a real external browser under E2E_TEST; the
  codex-oauth test drives the OAuth callback itself, and the browser was an
  orphaned process holding the harness pipes on the runner.

* fix(core): deflake hub daemon e2e tests on Windows runners

sdk-test on windows-latest fails intermittently in the hub daemon e2e
files:

- shutdown.e2e.test.ts dies with a bare 'Error: socket hang up'. That
  message is the ws handshake (http.ClientRequest) failing, not the
  /shutdown fetch (an undici failure prints 'TypeError: fetch failed'):
  a freshly spawned bun daemon on a loaded 2-core Windows runner
  occasionally drops its first accepted connection before writing the
  upgrade response. Real hub clients reconnect with backoff, and the test
  asserts shutdown behavior rather than first-connection reliability, so
  openAuthenticatedSocket now retries transient handshake failures within
  a 15s budget.
- singleton.e2e.test.ts times out waiting for daemon discovery: it still
  used the 10s hang guard that 0cfc90158 already raised to 30s in
  shutdown.e2e.test.ts for the same reason. Use the same 30s guard.
- Raise the e2e testTimeout to 60s so a test that legitimately spawns two
  daemons back to back can survive slow-runner startups instead of the
  discovery hang guard being cut off by the test timeout.

* feat(desktop): render tool output images as attachments (#13643)

* fix(desktop): render tool output images as attachments

Add support for displaying media returned by tool calls (e.g. screenshots)
as rendered images with expand-to-fullscreen capability instead of raw
base64 text. Introduces an `ImageCarousel` component for navigating
multiple images, propagates the expand handler to tool message blocks,
and extracts/validates output media in tool summaries.

* test: cover multi-image and canonical media extraction in tool output (#13645)

extractOutputMedia and the desktop tool-message rendering path were only
ever exercised with exactly one distinct valid image, and
canonicalInlineMedia (MCP-style type: "media" blocks for audio/video/file)
had zero coverage. Add tests for: multiple distinct images in one tool
result (parser + desktop carousel navigation), inline audio via the
mime_type key spelling, canonical video/file media blocks, and rejection
of an invalid canonical image block.

---------

Co-authored-by: Harrison <harrison@cline.bot>

* chore(desktop): release v0.0.20

* feat(sdk): add discovery boundary ahead of Agent Plugins support (#13017)

* ENG-2490: Propagate session aborts to teammates (#13647)

* fix(core): propagate session abort to teammates

* fix(core): persist aborted teammate tasks as cancelled

* fix(core): settle teammate work on session abort

* fix(core): isolate replacement runs from stale aborts

* refactor(core): narrow teammate task status metadata

---------

Co-authored-by: abeatrix <beatrix@cline.bot>

* fix(llms): use AI SDK 7 Langfuse telemetry (#13651)

* fix(llms): use AI SDK 7 Langfuse telemetry

* test(llms): cover Langfuse runtime context

* chore(llms): built-in model list update 1787907289186 (#13663)

* chore(llms): built-in model list update 1787907289186

Result of `bun run build:models`.
Includes updated model list and fixed formatting issues across codebase.

* test(llms): update GLM reasoning toggle expectation

* test: cover session search fallback on hub timeout and rejection (#13642)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

* test: cover sidecar search fallback on hub timeout and rejection

The existing search_sessions tests only exercised the index-hit and
empty-index-fallback paths with an immediately-resolved hub reply.
Add coverage for the two other realistic Hub-connection failure
modes the fallback is meant to tolerate: the hub call rejecting, and
the hub call hanging past the 750ms withSearchDeadline race.

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* fix(core): refresh Cline models from live catalog (#13670)

* feat(ui): share attachment drop zone (#13672)

* feat(ui): share attachment drop zone

* fix(ui): cancel disabled attachment drops

* chore(ui): simplify drop zone surface

* chore(ui): release v0.2.0-next.8

* Chore/bump undici mermaid (#13675)

* chore(deps): bump mermaid to 11.16.1 and raise undici floor to 7.29.0

* chore(deps): patch js-yaml and body-parser in the npm-managed subprojects

* fix(llms): make Langfuse tracer detection survive minified release builds (#13680)

* fix(llms): recognize direct tracer providers

* fix(llms): make Langfuse tracer detection survive minified release builds

Release binaries are compiled with minify enabled, which renames classes,
so initializeLangfuseTelemetry's constructor-name guard never matched
"ProxyTracerProvider" and silently returned readiness=false in every
production build (hub log: "creating span processor" followed by
"initialized readiness=false" with no branch message in between). Dev runs
execute unminified source, which is why the same env vars worked there.

Replace every constructor-name comparison with checks that survive
minification: detect the proxy structurally via getDelegate, distinguish a
recording provider from the no-op fallback by its lifecycle methods, and
confirm our NodeTracerProvider registration by object identity. When a
foreign provider already owns the global slot, attach the Langfuse span
processor to it when it accepts processors, and otherwise shut down the
orphaned provider and report the rejection instead of bailing silently.

Verified by bundling the module with Bun minify:true against the real
OpenTelemetry packages: the previous code reproduces readiness=false
(provider class name mangles to "H2"), the new code initializes with
readiness=true.

* fix(vscode): prevent hook spawn failures from crashing the core process (#13422)

* fix(vscode): prevent hook spawn failures from crashing the core process

A hook child-process spawn failure emitted "error" on HookProcess with no
listener registered, which Node's EventEmitter turns into an uncaught
exception - killing the entire cline-core process instead of failing the
one hook open. Guard the emit behind listenerCount so the rejection (which
StdioHookRunner handles) is the only propagation path.

The trigger was a workspace root that no longer exists on disk passed as
the spawn cwd: Node reports a nonexistent cwd as a misleading ENOENT on
the launcher binary ("spawn /bin/sh ENOENT"). Validate cwd existence in
HookProcess right before spawning - falling back to no explicit cwd with
a warning that names the missing directory - and when a spawn still fails
ENOENT because the directory vanished in between, name it in the error
message instead of blaming the shell.

* fix(vscode): fail hooks with a missing working directory instead of relocating them

Running a hook whose assigned cwd no longer exists from the host
process's own working directory would let its relative paths read and
write an unrelated location (e.g. the IDE install directory). Reject
before spawning, with an error naming the missing directory; the runner
reports the hook as failed and the task continues. Also carry pre-spawn
failure messages into HookExecutionError details so the cause is not
reduced to a bare "exited with code 1".

* fix(vscode): thread task id into hook runner creation so execution telemetry fires (#13547)

The SDK hooks adapter created every hook runner without a task id, and
StdioHookRunner gates all captureHookExecution calls on one being set —
so the next variant emitted zero hooks.execution events while discovery
telemetry fired normally. Pass the task id (and tool name for the tool
hooks) at all five factory.create call sites, and pin the threading
with a regression test.

* Desktop marketplace redesign: two-pane explorer with full catalog metadata (#13653)

* feat(desktop): add marketplace design exploration prototypes (storefront, explorer, registry)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): render catalog icon tiles without percentage padding

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): drop placeholder icon tiles from explorer marketplace direction

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): make explorer the marketplace view, drop design exploration harness

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): add category tag filters to marketplace explorer

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): collapse marketplace category pills behind a more toggle

* feat(desktop): remove maturity badges and CLI install section from marketplace

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* fix(core): propagate parent aborts to delegated subagents (#13677)

* fix(core): propagate parent aborts to delegated subagents

* docs(core): narrow delegated abort guarantees

* fix(core): release delegated sessions after execution

* fix(core): scope abort listeners to active runs

* fix(core): inherit parent runtime pid for subagents

* fix(desktop): keep Stop available for running child agents (#13678)

* fix(desktop): keep Stop available for running child agents

* fix(desktop): reconcile aborted tool activity

* fix(desktop): guard abort and agent polling races

* fix(desktop): preserve authoritative abort status

* fix(desktop): track queue-verified completion

* test(desktop): trim duplicate abort coverage

* fix(desktop): settle delayed queue verification

* fix: sanitize stored API keys and make provider credential rejections actionable (#13549)

* fix(vscode): sanitize pasted provider API keys at the settings write boundary

Clipboards smuggle control and invisible formatting characters (newlines,
zero-width spaces, BOM) into pasted API keys. The masked key field hides
the corruption and providers reject the key with a 401 indistinguishable
from a genuinely wrong key. Strip those characters and surrounding
whitespace once in the provider config store write path, so both backing
stores (legacy state secrets and providers.json) receive the clean value.
A whitespace-only value now clears the key.

* feat(llms,vscode): classify provider 401/403 as auth errors and surface actionable guidance

Add an "auth" ProviderErrorClass, assigned when the HTTP layer reports
401/403 — status-only on purpose, since provider bodies can quote words
like "unauthorized" without the request being an auth failure. The class
rides the existing errorClass plumbing (finish -> run-failed ->
AgentErrorEvent), so every host receives it with no new wiring.

In the VS Code chat surface, rewrite classified credential rejections
from BYOK providers into actionable text pointing at the API key
configuration, keeping the provider's raw body as a diagnostic tail.
Raw bodies alone are dead ends: Mistral, for example, answers an
identical {"detail":"Invalid API Key"} for a wrong, empty, or
wrong-scope key. Cline-account providers keep the JSON path so the
webview still renders their auth failures as a sign-in card.

* Fix ask-question option text not wrapping (#13718)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.21

* fix(core): stop an empty capability list from stripping image input (#13583)

`modelHasCapability` documents a missing or empty capability list as
carrying no signal, so each gate declares its own default. Two readers
bypassed it and read `capabilities` directly, where an empty list is not
nullish but `[].includes(x)` is false:

- the session runtime's `modelSupportsImages` metadata used
  `capabilities?.includes("images") ?? true`, so the intended fail-open
  never fired for an empty list and the file-read tool silently dropped
  every image from the request;
- `toProviderModel` projected an empty list onto `false`, telling pickers
  a model definitively lacks vision, attachments, and reasoning when
  nothing had been declared.

Both now route through the shared helpers, which state their unspecified
default explicitly: `modelSupportsImageInput` fails open for a capability
gate, and `declaredCapability` preserves `undefined` for `ProviderModel`'s
tri-state booleans. A populated list stays authoritative in both.

A thinking config now short-circuits `supportsReasoning` instead of being
OR-ed with the capability read, so its absence no longer collapses the
tri-state to `false`.

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* fix(llms): translate gateway capabilities in one place (#13584)

* fix(core): stop an empty capability list from stripping image input

`modelHasCapability` documents a missing or empty capability list as
carrying no signal, so each gate declares its own default. Two readers
bypassed it and read `capabilities` directly, where an empty list is not
nullish but `[].includes(x)` is false:

- the session runtime's `modelSupportsImages` metadata used
  `capabilities?.includes("images") ?? true`, so the intended fail-open
  never fired for an empty list and the file-read tool silently dropped
  every image from the request;
- `toProviderModel` projected an empty list onto `false`, telling pickers
  a model definitively lacks vision, attachments, and reasoning when
  nothing had been declared.

Both now route through the shared helpers, which state their unspecified
default explicitly: `modelSupportsImageInput` fails open for a capability
gate, and `declaredCapability` preserves `undefined` for `ProviderModel`'s
tri-state booleans. A populated list stays authoritative in both.

A thinking config now short-circuits `supportsReasoning` instead of being
OR-ed with the capability read, so its absence no longer collapses the
tri-state to `false`.

* fix(llms): translate gateway capabilities in one place

Three producers built gateway model definitions from catalog `ModelInfo`,
and each carried its own hand-written `switch` over the capability list.
Nothing tied them together, so they drifted:

- builtin providers always emitted a capability list, so a model whose
  catalog entry declares no capabilities became `["text"]` where the other
  producers emitted `undefined`. `modelSupportsToolCalling` fails open only
  for an absent or empty list, so that list read as an authoritative denial
  and stripped every tool definition from requests to the affected language
  models (dify, sapaicore, opencode, and the Codex CLI);
- the OpenAI-compatible path mapped an `audio` capability that
  `ModelCapabilitySchema` does not define, while the other two dropped it;
- the pass-through capabilities (`streaming`, `files`, `temperature`, ...)
  were enumerated explicitly in one, folded into `default:` in another,
  and ignored in the third.

One exported `toGatewayModelCapabilities` now serves every producer. It is
built on a `Record<ModelCapability, GatewayModelCapability | null>` rather
than a `switch`, so extending `ModelCapabilitySchema` without deciding the
new capability's mapping fails to compile instead of silently falling
through to a default.

The conformance tests walk the capability state space taken from
`ModelCapabilitySchema` itself and assert the real producers agree with the
translator, so a future producer that maps capabilities on its own fails
even when the translator's own unit tests still pass.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>

* fix(cli): keep markdown streaming prop stable to stop settle flash (#13719)

Flipping the <markdown> streaming prop from true to false when an
assistant text segment settles makes MarkdownRenderable call
updateBlocks(true), which skips every block-reuse path and destroys and
recreates all block renderables. Until tree-sitter re-highlights them
the whole message renders blank/unhighlighted, which users see as the
text flashing at the end of each response. Keep streaming={true} for
the transcript markdown (opencode's TUI does the same); entry.streaming
still drives the spinner glyph.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Clarify model-facing message when user rejects a tool call (#12673)

* Clarify model-facing message when user rejects a tool call

* Include the rejected tool's name in denial reasons

* Move user-rejected tool reason into @cline/shared

* Route new user-rejection approval paths through shared reason builder

Since the original PR, several new approval surfaces landed on main with
their own terse denial strings (CLI connectors, ACP permissions, Cline Hub
webview, desktop webview, example VS Code extension). Route all of them
through buildUserRejectedToolReason so the model sees a consistent,
non-error rejection message.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add buildUserRejectedToolReason to the @cline/shared integration-test stub

The VS Code integration tests run the tsc-built CJS tree and stub the
ESM-only @cline/shared package in test-setup.js; the stub was missing the
new export, so tool-approval-denial.js threw at module load in CI.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Trim scope back to the minimal rejection-copy fix

Restore the connector deniedReason plumbing, ACP permission strings,
desktop webview reason, example extension reason, and hub server fallback
to their main versions. Those surfaces already attribute the denial to a
user and are outside ENG-2329. Keep the Cline Hub webview change since
that path emits its own rejection string the model sees.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Move rejection guidance suffix into agent runtime per review

* Apply review suggestions: neutral fallback reason and -- separator before rejection suffix

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Default web search on for the desktop app (#13725)

* Default web search on for the desktop app

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make desktop web search default seed best-effort

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* fix(desktop): reconcile experimental sync behavior

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: JasmineLCY <38378321+JasmineLCY@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Haley Park <haleypark.design@gmail.com>
Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>
Co-authored-by: yzxcj797 <54314860+yzxcj797@users.noreply.github.com>
Co-authored-by: yzxcj797 <yzxcj797@users.noreply.github.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: 𝓜𝓲𝓼𝓼𝓪𝓻𝓲 𝓐𝓱𝓲𝓵 🌿 <143264692+missarii@users.noreply.github.com>
Co-authored-by: Dominic Cooney <dominic.cooney@cline.bot>
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Harrison <harrison@cline.bot>
Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: TheRealSpencer <32678829+TheRealSpencer@users.noreply.github.com>
2026-09-01 15:13:40 -07:00
Bee 1e4cdbda61 fix(desktop): sync reviewed Composio connectors onto desktop-experimental (#13739)
* Revert "feat(desktop): Composio connectors on desktop-experimental (#13685)"

This reverts commit 7844ae9250.

* feat(desktop): current Composio connectors state for desktop-experimental

Replaces the Aug 30 snapshot (#13685) with the reviewed state of
bee/poc-connectors-composio (PR #13684, Greptile 5/5): reverts the
snapshot commit, then applies the feature diff from that branch's
merge base, resolved against desktop-experimental's own changes
(cloud-agents flag wiring, marketplace layout).

This brings the fix for the beta-blocking bug - connector tools now
register in-process at session bootstrap from composio.json instead of
through a generated drop-in plugin, which packaged apps cannot load
(the plugin sandbox needs a JS runtime and bootstrap file on real disk
that the .app bundle does not ship). It also brings everything else
from review: durable cancellation tombstones with confirmed-revocation
pruning, fail-closed revocation error handling, serialized state
writes, per-slug delta reconciliation, connect/disconnect race
anchoring, zero-tools self-heal and warning, the @cline.bot
internal-feature gate with persisted account context, and the
env-var-only managed key model (no key in build artifacts).

* fix(desktop,core): revoke upstream grants on delete, scope flag cache per identity, paginate reconciliation

Review findings from johnwschoi on #13684 plus the follow-up Greptile
finding on #13739, all verified against the installed SDKs:

- Upstream revocation: @composio/core's connectedAccounts.delete never
  sends revoke_on_delete and the API defaults it to false - a soft
  delete that removes the Composio record while leaving the provider
  OAuth grant (the actual Gmail/Calendar/GitHub token) authorized after
  the UI reports a disconnect. All deletions now go through the raw
  client (getClient()) with revoke_on_delete=true, covering disconnects
  and every cancelled/abandoned-attempt revocation.

- Feature-flag identity scoping: FeatureFlagsService.setContext kept
  the previous identity's cached flag values, so after an account
  switch whose poll fails, the new account inherited the old one's
  flags (including internal-feature access). An identity change now
  resets the cache to defaults until a successful poll for the new
  identity, and constructor hydration skips a persisted snapshot
  written by a different known identity; the unresolved-at-startup
  fallback is preserved.

- Pagination: reconciliation listed only the first page of connected
  accounts while treating absence as "revoked remotely", so an account
  on a later page was disconnected locally. The listing now follows
  nextCursor to the end (capped), and absence-based removals are
  skipped entirely if the cap is ever hit.

- Disconnect id-guard: a redirect-less reconnect that finalized while a
  disconnect awaited the old account's revocation was blindly deleted
  from local state, orphaning its still-authorized account for the next
  refresh to import as a resurrection. Disconnect now removes only the
  exact account it revoked and stamps lastDisconnectedAt only when the
  removal took effect - the newer reconnect survives, consistent with
  newest-intent-wins everywhere else.

- Schema resilience: one stored tool schema that createTool rejects
  (e.g. an unsupported top-level allOf) threw during extension setup
  and blocked session initialization; registration is now per-tool
  fault-isolated (skip and log).

Regression tests for each: revoke flag asserted on every delete call,
identity-switch and failed-poll flag scoping, cross-identity persistent
cache, two-page reconciliation, disconnect-vs-reconnect race, malformed
schema skip.
2026-09-01 13:29:35 -07:00
John Choi 415be0e8f1 fix(desktop): support Linux helper builds on Windows (#13715)
* fix(desktop): support Linux helper builds on Windows

* chore(desktop): bump beta version to 0.0.21-beta.2
2026-08-31 13:47:43 -07:00
+14 36104ec1af chore(desktop): cut 0.0.21-beta.1 (#13713)
* fix(vscode): continue the surviving session on resume instead of rebuilding with the original task text (#13175)

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilt the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

The preserved conversation history is the source of truth on resume, so
the fallback prompt now just asks the model to reassess the history and
continue, matching the legacy resume prompt which also never resent the
original task. User-typed text still takes precedence when provided.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): continue the surviving idle session on follow-ups instead of rebuilding

Stopping a turn keeps the session alive, but every idle follow-up (bare
Resume after Stop, and typed follow-ups after a completed turn) tore
that session down and rebuilt it from persisted task history before
sending. Continue the matching idle session in place instead, the same
way the CLI reuses the live session after an abort. Rebuilding from
history now only happens when no live session matches the displayed
task (task opened from history, extension host reload).

A bare resume still needs a prompt to start a turn, so it sends the
neutral [TASK RESUMPTION] prompt (shared with the rebuild fallback and
hidden from the transcript); user-typed content is echoed and sent
as-is. If the send lands while the abort is still settling, the runtime
auto-queues it and drains once the abort completes.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): consolidate follow-up send paths in SdkFollowupCoordinator

Now that idle follow-ups continue the live session in place, the
two-mode sendToActiveSession helper was redundant: its non-queued branch
duplicated continueIdleSession minus the bare-resume prompt. Split it
into a single-purpose queueToActiveSession and fold the idle no-task
send into continueIdleSession, flattening askResponse's decision tree
to: queue onto a running turn, continue a matching live idle session,
rebuild from history, or abandon.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): reuse the existing neutral resumption prompt for bare resumes

Drop the newly invented long resumption wording in favor of the phrase
that already existed as the no-history fallback and that the transcript
hiding logic and test fixtures recognize: '[TASK RESUMPTION] Please
continue where you left off.' The net change to resumeSessionFromTask
against main is now just deleting the branch that resubmitted
historyItem.task as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilds the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

Bare resumes now always use the neutral prompt that already existed as
the no-history fallback; user-typed text still takes precedence. This
matches the legacy resume prompt (responses.taskResumption), which only
ever included user-supplied text as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): hide synthetic prompts from the queued-prompt echo

A send that races a settling abort is auto-queued by the runtime, so a
bare Resume can reach the pending_prompt_submitted echo carrying the
synthetic [TASK RESUMPTION] prompt. Echoing it leaked model-facing text
as a visible user bubble and shifted the visible-user-message ordinals
that edit/regenerate mapping relies on. Filter synthetic prompts with
isSyntheticUserPrompt, keeping user attachments visible (matching
isSyntheticSdkUserMessage semantics).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): preserve LiteLLM input token limits (#13293)

* fix(vscode): preserve LiteLLM input token limits

* fix(vscode): prefer live LiteLLM model metadata

* fix(vscode): generalize private catalog metadata

* test(vscode): preserve llms exports in vscode lm mock

* fix(vscode): point provider signup URLs at their API key pages (#13337)

* fix(vscode): point Mistral signup URL at the general API keys console

The Mistral provider's signup link led to the Codestral console, which
issues Codestral-scoped keys that fail with 401 on api.mistral.ai — the
endpoint the provider actually calls. Point it at the general API keys
page instead.

Fixes #13288

* fix(vscode): deep-link DeepSeek and Fireworks signup URLs to their API key pages

Both pointed at marketing homepages; link straight to the key-creation
pages instead, matching the rest of the registry and the desktop app's
provider-key-urls map.

* fix(ci): always build the legacy bundle from the legacy-extension branch (#13349)

The combined-VSIX workflow took legacy-ref as a free-form dispatch input
with no publish-time validation (next-ref has one: publish requires main).
Any typed ref — a PR merge ref, an unprotected branch — would be built
into the published VSIX by the environment-less build job, and the publish
environment approver only ever sees an opaque prebuilt artifact, so the
approval protected the marketplace PAT but not the shipped bytes.

Remove the input entirely and hardcode the protected legacy-extension
branch, which makes that branch's protection rules load-bearing for
releases. The tested-sha pinning between test-legacy and build is
unchanged. publish-extension skill dispatch command updated to match.

* fix(ci): lock the legacy publish workflow to the legacy-extension branch (#13350)

The branch dispatch input was a free-form string with no validation. Both
jobs checked it out and ran full npm lifecycle scripts from it: the publish
job next to VSCE_PAT/OVSX_PAT (and npm run publish:marketplace executes a
script from that same ref with the PATs in env), and the test job with NO
environment approval at all while inheriting the workflow-level
contents/packages/checks/pull-requests write grants. A dispatch pointing at
e.g. refs/pull/N/head would run outside-contributor code with the
marketplace keys behind one approval, or with a repo-write token behind
none.

Remove the input and hardcode the protected legacy-extension branch, drop
the workflow-level permissions to contents: read, and elevate only the
publish job to contents: write (tag push + GitHub release). The branch
input's default was legacy-extension, so normal publishes are unchanged.
publish-extension skill dispatch command updated to match.

* fix(vscode): SDK remote-config parity — refresh coordination, session gating, and fail-closed opt-out (#13226)

* feat(desktop): native notifications (#13166)

* feat(desktop): native notifications

* macos target

* fix(desktop): isolate macOS dev app identity

* fix(desktop): address notification review feedback

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched (#13310)

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched

Toggling an auto-approve setting while a task is open writes
autoApprovalSettings into the StateManager's task-settings overlay
(updateAutoApprovalSettings -> setTaskSettings). The SDK controller never
cleared that overlay on clearTask/showTaskWithId (the legacy controller
did), so after New Task the stale overlay kept shadowing global settings
in getGlobalSettingsKey(): toggle RPCs were accepted into global state,
but every posted state still carried the overlay's old version, which the
webview rejects as not newer - the auto-approve checkboxes froze forever.

Restore legacy parity in SdkTaskControlCoordinator: drop the overlay
(persisting pending writes first) in clearTask() and before installing a
different task's proxy in showTaskWithId().

Fixes #13260

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* changeset

* test(vscode): add end-to-end regression test for auto-approve freeze after New Task

Wires the real StateManager, the real updateAutoApprovalSettings handler,
and the real SdkTaskControlCoordinator.clearTask() together with the
webview's version gate modeled on ExtensionStateContext, pinning the
end-to-end invariant behind #13260: checkbox toggles must keep reaching
the webview after a mid-task toggle followed by New Task. Verified the
test fails when the clearTaskSettings() call is removed from clearTask().

* fix implicit any in regression test

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): show provider web-search support under the settings toggle (#13328)

* feat(desktop): show provider web-search support under the settings toggle

The global Web search toggle silently does nothing unless the session's
provider offers native web search, which made the setting read as if it
worked with any provider. The desktop General settings row now explains
that only providers with built-in web search honor it, and shows a live
status line: which connected providers are ready to use it (no extra
setup needed), or an amber warning with a link to the Models section
when none of them support it.

Support is resolved in the webview via a new providerOffersModelTool
helper in @cline/llms (browser export), sharing the same builtin-manifest
source of truth as the runtime's supportsModelTool attachment check.

* fix(desktop): address review — refetch web-search status on catalog invalidation, clarify per-model support

Greptile P2: the one-time catalog fetch could race an in-flight provider
save and show stale status; the row now refetches when the provider
catalog cache is invalidated (fired after saves complete).

Greptile P1: the ready line implied every model on the provider works;
Vertex excludes Claude routes, so the copy now scopes the promise to
models that support it.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* feat(ui/desktop): collapse finished runs into a work summary and remove hover-state dead space (#13315)

* feat(ui): add WorkActivity collapsed-run summary and float message actions as a pill

WorkActivity/WorkActivityTrigger/WorkActivityContent fold a finished agent
run's working rows (tool calls, thinking traces, narration) behind a single
"Worked for 4m 12s · 14 tool calls" disclosure built on the shared animated
disclosure primitives, with formatWorkActivityLabel/formatWorkDuration
exported for consumers.

Message hover actions no longer rely on the transcript reserving blank space
below each message: the action row is now a self-backed pill (border,
blurred background, shadow) that floats over whatever follows, so
conversations can pack rows tightly without hover chrome colliding with the
next message.

* feat(desktop): collapse finished runs into a work summary and tighten chat spacing

collapseCompletedWork post-processes the grouped transcript: once a run ends
on assistant text with no further tool calls, its working rows fold into one
expandable WorkActivity row while the final answer stays visible. Runs are
delimited by user messages; the trailing run only collapses when the session
has stopped running and actually produced an answer, so live streams and
cancelled/failed tails keep their rows. Assistant messages carrying images
or media are treated as deliverables and never collapse.

The conversation list gap drops from gap-8 to gap-4 now that hover actions
are self-backed pills that need no reserved space, and user messages add
their own top margin so turn boundaries stay visually distinct.

* refactor(ui/desktop): work summary label wording, flat expansion, stable in-run rhythm

Feedback round on #13315:

- Label reads "Worked for 4m 12s and made 14 tool calls" instead of joining
  with a dot; without a duration it falls back to "Made N tool calls".
- Expanded work rows render at transcript level — no rail or extra indent —
  since tool rows and thinking traces already carry their own nesting when
  expanded. The work content keeps the tight working-row rhythm.
- Live working rows (thinking traces + tool calls) now group into a 'run'
  render item with the same tight 0.25rem rhythm, so there is no oversized
  gap under a "Thought for Ns" row and every row keeps its exact position
  when the finished run folds into the work summary. A trailing
  answer-in-progress stays outside the group at transcript level, and pure
  prose spans keep normal spacing.
- The transient "Thinking..." indicator moves inside the transcript column
  and mirrors a trigger row's geometry, so the first real row replaces it in
  place with no jump.

* style(ui/desktop): hover-pill metrics, right-pointing work chevron, scroll and spacing fixes

Another feedback round on #13315:

- Hover action pill: +2px internal padding, a trailing inset after the
  timestamp (it sat flush against the pill border), and more clearance
  between the message content and the pill (2px -> 6px; the hover bridge
  grows to match).
- The work summary chevron points right while collapsed and continues
  counterclockwise to point up when expanded.
- Conversation bottom padding drops pb-20 -> pb-8: the composer sits below
  the scroller, so the padding only needs to clear a pinned action pill.
- Sending a message scrolls back to the bottom even if the reader had
  scrolled up (new AutoScrollOnSend on the user-message count, which ignores
  optimistic-bubble re-keying; @cline/ui now exports useConversation for
  this).
- An assistant answer directly under its run's working rows pulls itself
  0.5rem closer than the full transcript gap.

* style(desktop): leave a visible gap between a pinned action pill and the composer

pb-8 exactly matched the pill's ~40px footprint, so the last row's hover
actions sat flush against the composer top; pb-12 restores ~8px of daylight.

* style(desktop): widen the gap between the pinned action pill and the composer to ~24px

pb-12 left only ~8px of daylight under the pill; pb-16 reads comfortable
without reverting to pb-20's dead space.

* fix(desktop): keep the thinking indicator at the working-row offset mid-run

The indicator matched a trigger row's geometry but sat a full transcript gap
(1rem) below the last working row, while the tool/thinking row replacing it
joins the tight run group at 0.25rem — a visible upward jump. When the last
transcript item is working rows (or streamed assistant output), the
indicator now pulls up to the same tight offset; only at the start of a run,
under the user message, does it keep the normal gap.

* style(ui): calm the hover actions surface per team feedback

Borderless rectangle instead of the bordered pill: radius drops to
var(--radius), the side padding goes entirely (the icon buttons carry their
own hit areas), and the vertical padding halves. Blurred background and
shadow stay so it remains legible over following content.

* feat(ui/desktop): full-band hover reveal and iOS-style disclosure easing

The hover actions only appeared while the pointer was inside the message
box itself. The invisible bridge under each message now spans the full
height of the band the floating actions occupy (full row width), so
hovering anywhere in that strip reveals them. Sibling row types
(.cline-chat-tool, .cline-chat-work, and the desktop's run/tool groups)
become position: relative so they paint above the bridge — their own
content keeps its hover and clicks, and the bridge only wins in the band's
genuinely empty space.

All expandable rows (work summary, tool panels, thinking) open and close on
a 240ms symmetric ease-in-out cubic-bezier instead of the 60ms snap, with
chevron rotation on the same curve. Reduced-motion still disables both.

* revert(ui/desktop): drop the full-band hover reveal; quicken disclosure easing to 180ms

The full-band hover bridge (and the position: relative changes that made it
safe) is reverted per feedback — back to the narrow bridge that only spans
the gap under the message. The iOS-style ease-in-out on disclosures stays
but speeds up from 240ms to 180ms.

* fix(ui): recover live tool diffs that mount as a blank pierre skeleton

Live-streamed edit rows could show an empty diff for the whole run, with the
diff only appearing after the collapsed work row was expanded (fresh mount).
Root cause, confirmed by driving a live session and inspecting the element:
React StrictMode double-invokes @pierre/diffs' ref callback; the first
instance's async highlight work aborts on its immediate cleanup, and the
second instance adopts the abandoned half-rendered shadow tree as if it were
complete prerendered output — zero height, no code, no theme stylesheet,
permanently. A rendered diff always carries style[data-theme-css] in its
shadow root, so ToolFileDiff now checks for it shortly after mount and
remounts FileDiff (bounded attempts) when missing; the fresh host element
takes the normal render path and recovers within ~400ms. Verified live: the
diff now renders during the run.

* fix(desktop): keep interrupted runs expanded even with partial trailing text

The trailing-run collapse gated on 'ended with assistant text', which
misread a Stop that landed mid-answer as a finished run and folded the tool
calls the user wants to inspect. The gate is now the terminal status itself:
only completed (or restored-idle) sessions collapse the trailing run;
cancelled/failed/error tails keep their rows regardless of partial text.
(Greptile P1 on #13315 — matches the PR's stated rule.)

* feat(ui): share the markdown pipeline, chat polish, and ThinkingBlock across products (#13323)

* feat(ui): share the markdown pipeline, chat polish CSS, and ThinkingBlock

The desktop app and the cloud dashboard both consume @cline/ui yet rendered
assistant output differently, because Markdown policy and the thinking-trace
row lived app-side. This moves the shareable parts into the package:

- components/markdown (new export): the lazy Shiki code highlighter (GitHub
  light/dark, pinned language set) and agentMarkdownControls — the standard
  Streamdown configuration. streamdown/shiki/@shikijs/* become optional peer
  dependencies, mirroring @pierre/diffs.
- components/markdown.css: the desktop's chat polish moves in — chat-scale
  headings, outside list markers, single quiet code blocks with a
  hover-revealed copy control, table cards. Kept unlayered so it beats
  Streamdown's layered Tailwind utilities without !important.
- ThinkingBlock + formatThoughtLabel in agent-chat: the standard thinking
  row (brain icon, Thinking/Thought-for-Ns label, streaming shimmer, rail
  presentation, capped scrollable body). The shimmer and the
  reasoning-hover-suppression rule move into agent-chat.css; triggers gain
  the color transition the desktop applied locally.

Version bumps to 0.2.0-next.5 for the dashboard to pick up.

* refactor(desktop): consume shared markdown and thinking primitives from @cline/ui

The local Shiki highlighter, Streamdown controls, chat markdown polish CSS,
streaming-title shimmer, and reasoning hover-suppression rule are deleted in
favor of the @cline/ui versions (the highlighter test moves to the package's
suite). ReasoningBlock becomes a thin wrapper that hands MemoizedMarkdown to
the shared ThinkingBlock, and formatThoughtLabel re-exports from the package
so grouping code and tests keep their import path.

globals.css now imports @cline/ui/components/markdown.css (unlayered, so the
polish keeps beating Streamdown's layered utilities); the app keeps only what
is genuinely app-specific: link/image policy in markdown.tsx, selectability
rules, accent palettes, and the view-enter transition.

* style(ui/desktop): make thinking-trace prose legible

Thinking body text rendered too faint: plain muted-foreground plus the
desktop's font-thin weight. The shared thinking content now leans 75% of the
way back toward the body text color (still slightly de-emphasized), and the
desktop drops the thin font weight.

* ci(ui-publish): build @cline/shared before ui typecheck (#13354)

@cline/ui's generated-media imports @cline/shared/browser, which resolves to
shared's dist output. The build-shared step sat after typecheck/test/build,
so the first ui-publish dispatch since #13025 failed at Typecheck UI with
TS2307. Move the step to right after install.

* fix: run_commands object form without args routes through the shell instead of failing with ENOENT (#13336)

* fix: run_commands object form without args routes through the shell

The structured { command, args? } form of run_commands was always spawned
directly with shell: false. When a model emitted a full command line in
command with no args (e.g. { command: "echo hello" }), spawn failed with
ENOENT for any command containing a space, breaking command execution for
the whole session.

Direct exec now only applies when a non-empty args list is provided; the
object form without args is routed through getShellInvocation like the
string form. Schema descriptions are tightened so models put arguments in
args instead of embedding them in command.

Fixes #13279

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: trim structured-command schema descriptions

The union schema is only used for lenient validation of input the model
already sent; its descriptions never reach a model prompt. Keep them
short instead of restating executor behavior.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: simplify direct-exec comment in shell executor

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert: keep original structured-command schema description

The description never reaches a model prompt and the executor now handles
both shapes, so the wording change was cosmetic noise.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: gate direct exec on args key presence, not array length

Review feedback: an explicit empty args array is intentionally structured
input and stays direct exec; only an object with no args key is treated
as a full shell command line. Matches the key-presence rule already used
by the VS Code host's formatCommandForTerminal. Also replaces the
empty-args shell test (which was PowerShell-incompatible) with a test
pinning the direct-exec contract.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: normalize Gemini custom base URLs for legacy host-root values (#13329)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs: add GLM-5.3 to ClinePass models and reference pricing (#13357)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): stream run command output (#13179)

* feat(desktop): stream run command output

* fix(sdk): clean up detached command logs

* fix(sdk): reap detached logs after hub restarts

* fix(sdk): preserve live detached command logs

* fix(desktop): harden live command progress

* fix(sdk): recover detached logs for local hosts

* fix(desktop): reconcile command output tool rows

* fix(sdk): retain logs for surviving commands

* fix(core): prevent PID reuse from retaining detached logs

* fix(core): preserve detached logs on probe failures

* fix(core): retain detached logs during probe outages

* fix(desktop): resolve leftover merge conflict in messages projection test

Combine both sides of the assertion: main's incremented per-block
createdAt projection and this branch's toolCallId/hookEventName meta.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): make TUI dialog colors follow theme changes live (#13355)

* fix(cli): make TUI dialog colors follow theme changes live

Dialog content previously read the static palette constant, so open
dialogs (including the theme picker itself) kept the default dark-blue
accents while scrolling through theme previews. Add getDialogPalette /
useDialogPalette, which resolve dialog colors from the active theme's
dialog accents and re-render on every theme change, and migrate all
dialog-rendered components to it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(cli): derive dialog panel background from the active theme

Dark themes now lift their own background one OKLAB step for the dialog
surface, so panels keep the theme's hue instead of the library's fixed
#262626. DialogThemeSync pushes the surface into the dialog container
for new dialogs and repaints open panels, so the surface also follows
live theme previews. Light themes keep the neutral dark panel to match
the dark accent fallback and the light-on-dark dialog text.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: skill slash commands load via the skills tool instead of expanding into the user message (#13327)

* fix(desktop): show typed slash command instead of expanded skill markdown

The sidecar expands /skill and /workflow tokens into their instructions
before dispatching, so the runtime's persisted transcript only contains the
expanded text. After a turn (and when reopening a session) the webview
re-hydrates from that history and rendered the whole SKILL.md body as the
user's message; queue events echoing the expanded prompt could also add a
second user bubble, and fresh sessions were titled with the markdown's first
line. The CLI never shows this because its TUI keeps the typed text in its
own transcript and only sends the expanded prompt to the model.

Mirror that separation inside the desktop sidecar's display boundaries:

- history projection (readSessionMessages) inverts user text that starts
  with a configured command's instructions back to '/name remainder',
  which also repairs sessions recorded before this fix
- queue snapshots and chat_queued_prompt_start events echo the typed
  prompt recorded at expansion time, so the webview's optimistic-bubble
  re-key matches again
- an untitled session sent an expanded prompt gets titled from the typed
  command instead of the instructions' first line

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't overwrite a mid-turn rename with the typed-command title

The untitled check ran before dispatch, so renaming a fresh slash-command
session while its first turn was running got clobbered by the post-turn
typed-command title. Re-check at write time and only replace a missing title
or the one the runtime auto-derived from the expanded prompt.

Also documents the inherent prefix-inversion ambiguity flagged in review:
text hand-typed with a command's exact instructions persists byte-identically
to that command's expansion, so stored history alone cannot distinguish them.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop expanding skill commands; let the skills tool load them

Pasting the skill body into the prompt is why the transcript could ever show
it: the desktop webview re-hydrates from the runtime's persisted history, so
whatever the sidecar splices into the user message renders as if the user
typed it. The runtime already registers the skills tool, whose description
requires the model to invoke it whenever the user references a slash command
— so send the typed /skill text through and let the tool deliver the
instructions as a tool result (previously they arrived twice: pasted and via
the tool). The persisted user message, session title, and queue entries are
then simply the typed command, which deletes the typed-prompt registry, the
queue event/snapshot rewriting, and the title machinery from the previous
approach.

Workflows are not served by the skills tool and keep textual expansion, so
the read-time display inverter stays: it collapses expanded workflow prompts
— and skill prompts persisted before this change — back to the typed
/command in the history projection.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): option to keep skill slash commands typed for the skills tool

resolveRuntimeSlashCommandFromWatcher (and the hub snapshot proxy) accept
expandSkillCommands: hosts whose sessions register the skills tool pass
false so the typed /skill goes through and the model loads the instructions
as a tool result, keeping the persisted transcript as what the user typed.
Workflows always expand — the tool does not serve them. isSkillsToolAvailable
exposes the catalog check hosts use to decide (yolo preset and the skills
tool toggle leave textual expansion as the only delivery path).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): skill slash commands load via the skills tool instead of expanding

The TUI user-command wrap and buildUserInputMessage now keep a typed /skill
as-is when the session's mode/toggles register the skills tool, matching the
desktop app; workflows keep expanding, and yolo (zen) keeps expanding skills
because its preset has no skills tool. This also fixes CLI resume/history
surfaces showing the skill body: the persisted user message is now the typed
command.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): keep configured skill slash commands typed for the skills tool

expandSlashCommands no longer splices a configured skill's instructions into
the model text; the SDK session's skills tool delivers them as a tool result
(previously they arrived twice). Builtin pseudo-skills like /deep-planning
are not served by that tool and keep expanding, as do workflows.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): use the shared skill-expansion option in the sidecar

Replaces the sidecar's workflow-detection dance with core's
expandSkillCommands option and gates on isSkillsToolAvailable, restoring
textual expansion where the tool is missing (yolo mode or the skills tool
toggle) — a gap in the previous desktop-only change.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): drop the display inverter for expanded transcripts

Accepted trade-off to keep the change minimal: sessions recorded before
skills switched to the skills tool, workflow sends (deprecated), and
yolo-mode skill sends persist expanded instructions and now render that text
as-is instead of being collapsed back to the typed /command at projection
time.

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>

* Use fixed selection chevron in account dialog to match other dialogs (#13364)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): align system prompt with session mode (#13361)

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): finalize queued turns on chat_done with canonical history reconcile (#13330)

Turns that settle through the event stream (queued prompts, including the
first prompt of a fresh session) resolve their send() RPC early, so nothing
cleared the streaming shimmer or reconciled live-streamed content against
the persisted transcript at turn end. A turn whose deltas were incomplete
stayed visually streaming forever and only healed when a later non-queued
send rehydrated history.

chat_done (and chat_session_ended / the queue-drain double check) now clears
the active assistant streaming id and schedules a short-delayed
read_session_messages + applyCanonicalHistory, guarded by turn epoch,
session id, and in-flight send submissions so it never clobbers a newer
turn or duplicates the blocking send path's own finalization.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.14

* fix(clients): filter non-chat models from chat pickers (#13317)

* fix(clients): filter non-chat models from chat pickers

* fix(clients): align chat model eligibility

* fix(desktop): strip user_input envelope when copying a user message (#13369)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>

* docs: show DeepSeek V4 peak and off-peak pricing (#13312)

* docs: update DeepSeek V4 average pricing

* docs: show DeepSeek peak and off-peak pricing

* docs: add GLM-5.3 reference pricing (same as GLM-5.2)

* docs: add GLM-5.3 to ClinePass models table

* fix(llms): display billed gateway cost (#13385)

* fix(shared): run PowerShell commands with fail-fast error semantics (#13358)

* fix(shared): run PowerShell commands with fail-fast error semantics

The run_commands PowerShell wrapper never set $ErrorActionPreference, so
the default 'Continue' applied: a pipeline erroring per item (e.g. a
malformed Where-Object over Get-ChildItem -Recurse) emitted one error
record per enumerated file - tens of thousands of stderr records on
large trees, looking like a hang - and could still resolve as SUCCESS
with exit 0.

Prepend $ErrorActionPreference='Stop'; to the script content executed
by the ScriptBlock so the first error terminates the command with a
non-zero exit and a single error message. Concatenated on the same line
as the user command so error line numbers stay unshifted.

Fixes #13285

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(shared): set the fail-fast preference in the bootstrap scope

Setting $ErrorActionPreference='Stop' by string-prepending it into the
scriptblock source displaced a leading param(...) from its mandatory
first-statement position, so scripts beginning with a param block failed
with CommandNotFoundException. Preference variables are dynamically
scoped, so setting Stop in the -Command bootstrap gives the invoked
scriptblock identical fail-fast semantics while keeping the user script
byte-identical (param works, error positions unshifted) and drops the
doubled-quote escaping.

* docs(shared): document the fail-fast tradeoffs in the PowerShell wrapper

Stop promotes every non-terminating error, not only per-item pipeline
floods: partial-result commands (recursive listings over access-denied
junctions) now stop at their first error, and Windows PowerShell 5.1
turns in-script stderr redirection of succeeding native commands fatal.
State this in the wrapper comment as a deliberate tradeoff, with the
GitHub Actions precedent and the per-command opt-outs.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* ci: stop over-long changelogs from silently dropping release Slack posts (#12955)

Slack section blocks reject text longer than 3000 characters. The Slack
action logs that rejection as ##[error] but does not fail the step, so an
over-long changelog drops the release announcement while the run stays
green — cline@3.0.50 (3272 chars) published to npm, tagged, and cut a
GitHub release with no Slack post and nothing red to notice.

Every publish workflow pasted the changelog section verbatim into one
section block, so all six were exposed; the SDK, desktop, and extension
sections were only 150-350 chars under the ceiling.

Add a slack_content output alongside content: unchanged when the section
fits, otherwise trimmed on a line boundary with a link to the full
release notes. Only the Slack payload uses it — GitHub release bodies and
the desktop updater manifest still get the whole section.

* ci: tidy workflow cache config and job permissions (#13403)

Publish workflows now always do clean npm installs (no dependency
cache in their test gates), the e2e workflow's cache keys are
exact-match only, and the e2e job drops an id-token permission it
never used.

* Rename desktop app from "Cline Code" to "Cline" (#13401)

* Rename desktop app from Cline Code to Cline

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Format touched Rust test assertions

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): surface provider-executed tool activity as observational events (#13300)

* fix(llms): surface provider-executed tool activity as observational events

Provider-executed tool parts (e.g. every tool the Claude Code CLI runs
inside its own session) were dropped by the model-tool guard added for
web search: only declared model tools were re-emitted, everything else
hit continue with nothing yielded. Those sessions modified the workspace
with no tool activity in runtime events, transcripts, or the UI.

Route all providerExecuted parts onto the observational path instead:
emit execution-tagged tool-call-delta and tool-result events, matched by
tool-call ID for providers that omit the flag on the result half. They
stay out of AgentRuntime's execution/approval loop, and the runtime
already persists them as modelToolActivities and projects them for
display.

The AgentModelEvent tool-result variant widens toolName from
ModelToolName to string to carry the provider's own tool names.

* fix(agents): keep turns that are only provider-executed tool activity

A turn consisting solely of observational tool activity has an empty
assistant content array - the activity lives in message metadata, since
projecting it into content would replay tool_use blocks the model never
gets results for. The empty-content guard threw on such turns, erroring
the run and losing the activity from the transcript. Count model-tool
activity as content for the emptiness check (error finishes still
throw); replay stays safe through the codec's empty-content placeholder.
Also drop the trailing text delta from one gateway test so the tool-only
stream shape stays covered end to end.

* feat: allow agents to create scheduled tasks (#13331)

* feat(core, desktop): add durable todo agenda

* fix(desktop): secure todo approvals and track tool usage

* fix(desktop): clean up failed approval delivery

* fix(desktop): authenticate approval connections

* fix(desktop): cancel approvals on broadcast failure

* fix(desktop): authenticate development approvals

* fix(desktop): harden development approvals

* test(core): make task paths cross-platform

* fix(desktop): serialize approval readiness

* refactor(core): unify todo and schedule tools

* feat(core): distinguish user todos from agent suggestions

* fix(core): hide tasks tool in yolo mode

* fix(core): enforce schedule workspace scope

* fix(core): bind schedule scope to hub connection

* fix(core): establish task scope at hub startup

* fix(core): scope task automation by workspace

* test(core): normalize workspace path expectations

* test(core): serialize Windows CI workers

* fix(core): reject unregistered schedule authority

* fix(desktop): guard task execution commands

* fix(core): avoid polynomial regex in mention parsing

* fix(core): address schedule tool review feedback

* fix(core): bind websocket clients to hub workspace

* fix(core): flatten tasks tool input schema

* fix(core): authorize multi-workspace hub clients

* test(core): type hub transport authority mock

* fix(cli): register a workspace client for remote schedule commands (#13398)

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): treat ClinePass as OAuth-managed in the chat credential gate (#13404)

* fix(desktop): treat ClinePass as OAuth-managed in chat credential gate

ClinePass shares the Cline account OAuth credentials (its auth handler
stores under the "cline" provider), so the webview never sees a plain
API key for it. The chat pre-flight check only exempted cline/oca/
openai-codex, so switching to ClinePass while signed in via OAuth
blocked with "Missing API key" even though the sidecar resolves the
stored access token fine (which is why the CLI worked).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: format helpers.test.ts with biome

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): stack code block lines when streamdown lineNumbers is off (#13412)

streamdown renders each Shiki token line as a bare inline span with no
newline text between non-empty lines, and only applies its block line
class when lineNumbers is on. With lineNumbers off (the desktop app's
config) every multi-line fenced block collapsed into one run-on line.
Make the direct line spans under code-block-body display: block in the
shared markdown.css; empty lines keep their height via their lone "\n"
child under white-space: pre.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): work summary undercounts wall time when pre-tool thinking attaches to the answer (#13413)

* fix(desktop): anchor work summary duration on the answer row, not attached pre-tool reasoning

The collapsed 'Worked for Xs' row undercounted wall time whenever a turn's
assistant message contained thinking + tool_use with no narration text: the
canonical projection emitted the reasoning-only row after the tool row (both
stamped before the tool executed), the webview attached that row to the final
answer, and collapseCompletedWork used the answer's earliest attached
reasoning timestamp as the end anchor - excluding the entire tool execution
(e.g. 'Worked for 5s' for a turn with an 8s command).

- webview: end the work span at the answer row's own timestamp, clamped to
  the last collapsed row so a fallback answer bubble with a synthetic early
  timestamp cannot shrink the duration either
- sidecar: flush pending thinking before a tool_use row so rehydrated
  transcripts keep the live-stream order (thinking before its tool call) and
  pre-tool reasoning no longer rides on the next answer

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep interleaved thinking between the tool calls it separates

Address Greptile review: when one assistant message interleaves thinking
between multiple tool_use blocks, each reasoning segment now projects at its
own position (attached to a text row from its own segment when present,
otherwise as its own row) instead of merging into the first reasoning row,
which displayed later thinking before a tool call it actually followed.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): remove settings gear hover state while Account screen is open (#13408)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't show "No sessions found" while session history is still loading (#13414)

* fix(desktop): don't show 'No sessions found' while session history is still loading

Replace the isLoadingHistory flag with hasLoadedHistory, set only once the
backend has actually answered a list_discovered_sessions request. The sidebar
and Sessions view now keep their loading state until that first definitive
response, so the empty-state copy can no longer appear while history is still
being fetched (or while a failed fetch is being retried).

Also retry a failed initial fetch on the 2s event cadence instead of stranding
the UI until the 12s periodic poll, which is what stretched the misleading
empty state to ~10 seconds after a webview reload when the websocket lost the
race with the page load.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop history fast-retry from re-arming after hook unmount

A failed initial fetch that settles after the hook unmounted could schedule a
new retry timer after cleanup had already cleared the refs, leaving the
abandoned hook polling the backend every 2s. Guard scheduleRefresh with a
disposed ref set by the mount effect's cleanup.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix @ file mentions breaking on paths with spaces (#13391)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with "command not found" on VS Code 1.134 (#13402)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with 'command not found' on VS Code 1.134

Code action commands carried arguments (expandedRange, diagnostics),
which routes them through VS Code's CommandsConverter cache. VS Code
1.134 disposes the cached entries before the clicked action executes,
so every lightbulb action failed with 'Actual command not found,
wanted to execute cline.addToChat'.

Drop the arguments so the command id is passed through directly, and
recover the context in the handler instead: getContextForCommand now
expands an empty selection by 3 surrounding lines (matching the old
provider behavior) and gathers document diagnostics intersecting the
range when none are passed explicitly.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Scope gathered diagnostics to the selection/cursor

Match the old CodeActionContext.diagnostics behavior: only include
diagnostics intersecting the range the action was requested for, not
the surrounding lines the text gets expanded to.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop: unify Plugins, MCP, and Skills into one Plugins hub with a dedicated Marketplace page (#13411)

* Unify desktop plugins, apps, MCP, and skills into one Plugins hub with a Browse directory mode

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Open the marketplace directory as a modal over the Plugins hub instead of swapping the page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename directory to Marketplace: Browse Marketplace button, Marketplace modal title with icon, search placeholder

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix search input focus ring clipped by the Marketplace modal scroll container

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Address Greptile review: keep selected tag chip visible when its count drops to zero, and remount installed tab when a marketplace install completes after the modal closed

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Track marketplace modal mutation flag in a ref so a close click racing a queued render cannot skip the inventory remount

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make Marketplace its own settings page under Customizations and restore Channels as a standalone page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Remove icon from Marketplace page header for consistency with other settings pages

* Notify mounted inventory views when the marketplace invalidates the cache so late install completions refresh the Plugins hub

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop/ui): recommended and free model tiers in the composer model selector (#13410)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK (#13415)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): recommended-feed badges and descriptions in provider settings (#13416)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* feat(desktop): recommended-feed badges and descriptions in provider settings

Review suggestion on #13410: the provider settings page has room for
more model detail than the composer's picker. The cline/cline-pass
provider cards now refresh their model list through
list_provider_models (the catalog snapshot deliberately skips the
recommended-feed overlay so the startup catalog fetch never blocks on
the feed) and render Recommended/Free tier badges plus feed tags (NEW)
next to the model name, with the model description underneath. The
refreshed list also surfaces the live entries instead of the bundled
snapshot.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): scope the settings featured model list to its provider and revision

The fetched featured list was unscoped component state: switching
between cline and cline-pass reused the component instance, so the
previous provider's models stayed visible while the new request was
pending (or forever, when it failed), and the retained copy shadowed
later provider.modelList updates — adding a second custom model
submitted the stale list as the complete configuration and dropped the
first addition.

The fetched list now only applies to the provider and modelList
revision it was fetched for (falling back to the catalog snapshot
otherwise and refetching on membership changes), and add-model submits
the union of the displayed and configured ids so an update can never
silently unconfigure existing entries.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): update packed-Tailwind smoke contract for the picker's max-h-64 (#13421)

The ui-publish smoke check pins a set of Tailwind candidates the packed
sources must emit; #13410 grew the SearchCombobox options list from
max-h-56 to max-h-64, so the publish run failed on the stale candidate.
All other pinned candidates verified against the current sources.

* feat(desktop): refresh app icons and branding (#13400)

* ci(vscode): upload E2E failure recordings from the right path (#13427)

The job sets working-directory: apps/vscode, but that default applies to run
steps only, not to `uses:` steps. Since #10961 moved the extension under apps/
and added that default, the artifact path has resolved against the repo root,
matched nothing, and every failing run logged "No files were found with the
provided path: test-results/playwright/" instead of uploading recordings.

Widen to test-results/ so Playwright's error-context snapshots ship alongside
the videos.

* fix(hooks): deliver tool hook contextModification to the model (#13297)

* fix(hooks): deliver tool hook contextModification to the model

On the next engine, a tool_call (PreToolUse) hook's contextModification
was parsed into HookControl.context and then silently dropped: the
runtime beforeTool/afterTool result contract had no channel for
injecting conversation context. Legacy consumed it (ToolExecutor /
ToolHookUtils pushed <hook_context> blocks into the next user turn), so
this was a regression of documented behavior.

- Add appendContext to AgentBeforeToolResult/AgentAfterToolResult.
- AgentRuntime collects appendContext across hooks during an
  iteration's tool executions and appends one <hook_context> user
  message after the tool results, keeping tool-result parts contiguous.
- Map HookControl.context into appendContext in both subprocess hook
  layers (skipped when the hook cancels, matching legacy, where the
  message doubled as the error).
- Truncate injected context at 50KB per hook output, matching legacy.
- Concatenate appendContext across merged hook layers.

tool_result (PostToolUse) hooks still run detached with stdout ignored;
making them blocking so their context can be collected is a follow-up.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): stamp tool identity on injected hook context blocks

Contexts are batched into one message after the tool results, and
parallel tool execution collects them in completion order, so position
alone cannot attribute a block to its tool call. Add tool_name and
tool_call_id attributes to each <hook_context> block.

* fix(hooks): sanitize hook context block markup

Attribute values (tool_name, tool_call_id) are stripped of quote/angle
characters and embedded </hook_context> closers in hook output are
neutralized, so neither provider-supplied ids nor hook text can corrupt
or spoof a block's stamped identity.

* fix(hooks): neutralize forged opening hook_context tags in hook output

The previous sanitization only neutralized closing tags, so hook output
could still open a forged <hook_context> block claiming another tool's
identity. Escape both opening and closing embedded tags with one rule.

* fix(hooks): hide injected hook context from user-facing transcripts

Stamp the injected hook-context user message with displayRole 'system'
(the compaction-summary convention) so it reaches the model but does
not render as a user bubble in live or replayed transcripts. Without
this, resuming a session showed the raw <hook_context> block as if the
user had typed it.

* fix(hooks): neutralize case-variant embedded hook_context tags

The tag-neutralization regex was case-sensitive, so hook output could
still smuggle a forged tag as <HOOK_CONTEXT>. Match case-insensitively.

* fix(vscode): map PreToolUse contextModification into runtime appendContext

The extension's hooks adapter bridged file hooks into the SDK runtime
but forwarded only cancel/errorMessage, so a PreToolUse hook's
contextModification never reached the model. Map it into the runtime's
appendContext channel; HookFactory already truncates it at 50KB.

* fix(vscode): hide hook-injected context from replayed transcripts

Live sessions never rendered the injected <hook_context> user message,
but session reload replayed it as a user bubble (and post-resume turns
kept doing so). Treat these messages as synthetic in the user-message
mapping: honor the displayRole 'system' stamp the runtime sets, with a
text-prefix guard for paths where metadata is unavailable. This also
keeps edit/regenerate ordinal mapping aligned with visible bubbles.

* fix(hooks): run file hooks through exactly one layer per host

The VS Code extension registered two independent hook execution layers:
its own hooks adapter (config.hooks) and the SDK core's file-hook
extension from the runtime bootstrap. When both discover the same hook
files, every hook executes twice per event — and with context injection
wired, each contextModification would be injected twice.

Add a 'hooks' runtime config extension kind (in the default set, so the
CLI keeps core file hooks unchanged) and gate the bootstrap's file-hook
extension on it. The extension excludes 'hooks' at session start, so
its adapter — which also provides the hook status UI and the
hooksEnabled setting — is its single execution path.

* fix(vscode): discover hooks from the session workspace, not only global state

Hook discovery read workspaceRoots from global state shared across
every Cline instance, so another window repointing it made workspace
hooks silently stop being discovered. With the extension's adapter now
the single hook execution layer, that meant no hooks at all.

HookFactory takes an optional sessionWorkspaceRoot and unions that
root's .clinerules/hooks into discovery (and into cwd resolution), fed
from the session config's cwd. Shared-state discovery still works, so
behavior in the single-window case is unchanged.

* fix(hooks): keep sanitized hook attribute values distinguishable

Replacing every markup delimiter with the same underscore could
collapse two tool call ids that differ only by such a character into
identical stamps. Escape each delimiter with a distinct token instead.

* fix(hooks): make hook attribute sanitization injective

Escaping the underscore itself turns the attribute escaping into a
uniquely decodable code, so no two distinct tool call ids can collapse
to the same sanitized stamp (previously an id containing a literal
escape token could collide with an id containing the delimiter).

* fix(vscode): reconstruct hook status rows when replaying transcripts

hook_status messages are emitted live but never persisted, so reloading
a session dropped every hook row. The injected <hook_context> blocks
carry the hook source and tool name, so the replay translator now
rebuilds a completed hook status row from each block. The injection is
also no longer treated as a user turn boundary, so the final turn's
completion retag is unaffected by it.

* fix(hooks): collect PostToolUse hook output and honor its control (#13298)

* fix(hooks): collect PostToolUse hook output and honor its control

tool_result (PostToolUse) hooks ran fire-and-forget with stdout
ignored, so their entire JSON output — contextModification and cancel —
was discarded. Legacy awaited PostToolUse, injected its
contextModification into the conversation, and honored cancel.

- Run tool_result hook commands blocking (same 120s default timeout as
  tool_call) in both the hook-config-file layer and the agent-hook
  subprocess layer.
- Map their output: cancel stops the run with the hook's error message
  as the reason; otherwise context is injected via afterTool
  appendContext.

This restores legacy blocking semantics: tool results now wait for
tool_result hooks, but only in sessions that have one configured.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): bound tool_result hook wait and isolate cancel reason

Address review findings:
- The agent-hook subprocess layer forwarded an unset timeoutMs
  unchanged, so a tool hook command that never exits would block the
  agent indefinitely. Default both tool_call and tool_result to the
  120s bound the hook-config-file layer already used.
- A cancelling hook's error message was folded into the same context
  field as other hooks' injectable context, so merging controls could
  leak unrelated hook context into the cancellation reason. Carry it as
  a separate cancelReason, and surface it as the stop reason for
  beforeTool cancels too.

* fix(hooks): prefer errorMessage as a cancelling hook's stop reason

When a cancelling hook returns both contextModification and
errorMessage, the context-first parse precedence made the injectable
context the cancel reason and discarded the actual error. Parse the two
fields separately: errorMessage wins as the cancel reason (matching
legacy), and a lone errorMessage still folds into injectable context
for non-cancelling hooks as before.

* fix(vscode): honor PostToolUse hook cancel and contextModification

The adapter awaited PostToolUse hooks but discarded their output
entirely. Map cancel to a stop control (with errorMessage as the
reason) and contextModification into the runtime appendContext channel,
matching the PreToolUse mapping and legacy semantics.

* fix(hooks): whitespace-only errorMessage no longer suppresses the cancel reason

A cancelling hook returning meaningful context alongside a blank
errorMessage lost both: the parsers selected the whitespace as the
reason and the result mappers trimmed it away. Require a non-blank
errorMessage before it wins, so context serves as the fallback reason.
Apply the same fallback in the extension adapter's stop mapping.

* fix(core): stop Windows CI worker crashes from the agenda spec watcher (#13428)

* fix(core): watch agenda task specs via the resolved long path

fs.watch on a path with 8.3 short components (e.g. C:\Users\RUNNER~1
temp dirs) trips a libuv assertion in fs-event.c on Windows and aborts
the whole process. Since the agenda task manager landed, every hub
server test spins up its spec watcher on such a path on hosted Windows
runners, killing the vitest worker and failing the sdk-test Windows job
on every branch. Resolve the specs dir with realpathSync.native before
watching so libuv only ever sees the long form.

* test(ui): stub ResizeObserver for @pierre/diffs in tool-diff tests

jsdom does not implement ResizeObserver, so every ToolFileDiff render
logged a ReferenceError from @pierre/diffs to stderr. Tests still
passed; this just silences the noise the same way the constructable
stylesheet shim does.

* fix(core): skip the agenda spec watcher when the dir does not resolve

Falling back to the raw path on realpath failure would reintroduce the
Windows short-path abort; log and go without the watcher instead.

* fix(vscode): honor the classic truncation range when migrating legacy tasks (#13419)

Classic Cline truncated long conversations by omitting an index range of
api_conversation_history from every API request (keep the first
user-assistant pair, drop everything through the range end, strip
orphaned tool_results from the first kept message). The range was
persisted on the history item while the full history stayed on disk.

legacyApiHistoryToSdkMessages ignored conversationHistoryDeletedRange
and converted the entire file, so resuming a migrated long task handed
the SDK an untruncated working context that could exceed the model's
context window by millions of tokens - every request failed with
'prompt is too long' and every compaction restarted from the full
history (#12996, confirmed by the reporter: the task was migrated from
an older version and broke after a restart, with each compaction
starting from ~3M tokens).

The migration now replays exactly what the classic extension sent:
slice out the deleted range and drop orphaned tool_results, mirroring
ContextManager.getTruncatedMessages (see origin/main). Malformed ranges
fall back to the full history (previous behavior).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): show the diff edit view for multi-line edits in CRLF files (#13417)

The edit preview computed proposed content with an exact old_text match, but
the SDK executor normalizes old/new text to the file's own line endings before
matching (#12305) - reads strip CR, so models emit LF-only text even for CRLF
files. Any multi-line old_text in a CRLF file therefore failed the preview's
match: the diff edit view silently never opened while the executor applied the
edit. Single-line edits (no line break in old_text) were unaffected, which is
why the diff view appeared to trigger inconsistently.

Mirror the executor's EOL normalization (and its literal $-sequence insertion)
in the preview computation.

Fixes #13296

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): report truthful session status so desktop checkpoint restore stops wedging (#13418)

* fix(core): keep hub session status truthful across queue-drained turns

Queue-drained turns settle only through the event stream, but the hub
runtime host mistranslated their lifecycle in two ways:

- session.updated events carrying only a snapshot (persistence updates)
  defaulted the projected status to "running". When one trailed the
  final idle update after a turn, clients that track busy state from
  status events (the desktop sidecar's workspace restore gate) stayed
  busy forever. Use the snapshot's real status and emit nothing when
  neither source reports one.
- the per-run agent.done dedup was only reset by run.started, which the
  daemon-side queue drain never publishes, so a drained turn's done was
  swallowed as a duplicate of the previous turn's. Reset the dedup on
  session.pending_prompt_submitted, and suppress stale run.completed
  events that land inside a drained turn's window so they can neither
  emit a phantom done nor consume the drained turn's dedup slot.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(desktop): cover restore unlock after an event-settled queued turn

Exports the sidecar's core-session event handler so the queued-turn
lifecycle (busy via status events, cleared by the done agent event,
restore allowed afterwards) is testable end-to-end.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): start interactive sessions without a prompt as idle

The runtime host reported every new session as "running" until its
first turn ended. Interactive hosts (the desktop app) start sessions
with no prompt and dispatch turns through separate send calls, so a
created-but-never-prompted session stayed "running" forever — wedging
clients that gate workspace operations (checkpoint restore, message
edit) on active turns.

Interactive no-prompt starts now begin idle, start emits the session's
actual status (resumed sessions no longer masquerade as running), and
markTurn* transitions keep tracking in-memory status for lazily
persisted sessions so the first turn still reports running -> idle.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: format hub-runtime-host test filter

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop the drained-turn done bookkeeping, keep the minimal fix

The stuck restore is fully explained by the two status defects (fabricated
"running" from snapshot-only session.updated events, and never-prompted
interactive sessions reporting "running"). The done-dedup machinery for
queue-drained turns addressed a separate cosmetic gap (queued turns emit no
chat_done, pre-existing) and required fragile run-window heuristics, so it
is removed to keep this change reviewable. Sidecar test now settles the
queued turn through the status event, matching the shipped mechanism.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs(sdk): document the truthful session-status contract

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(deps): update Langfuse packages and bump app versions (#13443)

* chore(deps): update Langfuse packages and bump app versions

Update @langfuse/otel to v5.10.1 and add @langfuse/vercel-ai-sdk v5.9.1 for improved observability with Vercel AI SDK.

Bump versions for @cline/code to 0.0.14 and @cline/ui to 0.2.0-next.6, updated via bun.lock.

Other Changes:
Added optional userId to AgentRuntimeConfig.
Propagated userId, sessionId, conversationId, runId, iteration, provider, and model context into AI SDK telemetry.
Added AI SDK 7 runtimeContext with explicit includeRuntimeContext.
Added stable OTEL_SERVICE_NAME=cline-sdk.
Added runtime metadata assertions in agent tests.

* add taskId

* Revert "add taskId"

This reverts commit f20d31d96d.

* docs: simplify Open Cline step in installing guide (#13405)

* docs: remove duplicate GLM-5.3 rows in ClinePass tables (#13449)

Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>

* chore(sdk): release v0.0.76

* chore(cli): release v3.0.56

* docs(cli): scope the v3.0.56 release notes to CLI-visible changes

* feat(desktop): interactive welcome hero graphic (#13399)

* feat(desktop): add interactive welcome hero

* feat(desktop): support composable welcome hero variants

* feat(desktop): reskin first-run onboarding (#13441)

* refactor: centralize client tool availability (#13451)

* chore(sdk): release v0.0.77

* docs(cli): drop the tasks tool from the v3.0.56 notes, it is desktop-only

* chore(vscode): prepare 4.1.11 release

* chore(desktop): release v0.0.15

* fix(vscode): remote config MCP settings (#13466)

* fix(vscode): enforce enterprise MCP controls on the Customize marketplace

The unified Customize marketplace replaced the old MCP marketplace
without carrying over enterprise remote-config enforcement: the catalog
RPC returned every MCP entry and installs were never policy-checked,
so orgs with mcpMarketplaceEnabled=false or an allowedMCPServers
allowlist saw (and could install) all marketplace MCP servers.

- Filter MCP entries out of getMarketplaceCatalog when the marketplace
  is disabled, and restrict entries to the allowlist when configured
  (matching entry id, display name, installed server name, or source
  repo URL, mirroring legacy GitHub-URL allowlist ids)
- Reject installMarketplaceEntry requests that violate the policy
- Map the published catalog's repo/homepage fields onto
  sourceUrl/homepageUrl so URL-based allowlists can match
- Update the enterprise MCP server controls docs

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: simplify MCP marketplace policy enforcement

Fold the policy check into marketplace-helpers, drop the dedicated
test suite, and trim the docs edit to the strictly necessary line.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Treat an empty preserved capability list as unspecified when seeding tools (#13465)

* Treat an empty preserved capability list as unspecified when seeding tools

toSdkModelInfo guarded the tools seeding with a strict
preservedCapabilities === undefined check, but modelHasCapability —
the runtime's own reader — treats undefined AND length === 0 as
"unspecified". A custom OpenAI-Compatible model whose stored
capabilities field is a defined-but-empty array (a config carried over
from before the field existed, or one round-tripped through a boundary
that defaults it to []) skipped the seeding; the first boolean
projection to run afterwards (e.g. supportsReasoning) then populated
the array, the runtime gate read the non-empty, tool-less list as
authoritative, and every tool definition was silently dropped from the
session (#13463).

The guard now covers the empty array too, matching the reader's
unspecified semantics.

* test: satisfy the store's isModelInfo gate so the empty-capabilities case actually reaches knownModels

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: yzxcj797 <yzxcj797@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>

* chore(vscode): prepare 4.1.12 release

* Add feature flags to the desktop app (#13289)

* Add feature flags to the app

* React to account updates

* Address comments

* use a per-app file

* fix: propagate Langfuse session telemetry (#13473)

* fix telemetry session propagation

* feat telemetry client version metadata

* fix(core): address Langfuse review feedback — hub client identity + delegated agent session grouping (#13475)

* fix(core): rebuild hub session client identity from request headers

Hub-backed sessions do not transport extensionContext (it is local-only),
so the daemon's runtime built traces without the clientName/clientVersion
metadata even though the hub client bakes X-CLIENT-TYPE / X-CLIENT-VERSION
into the session's provider headers. Reconstruct extensionContext.client
from those headers during local runtime bootstrap so hub-backed Langfuse
traces carry the same client identity as local runtimes, and the daemon's
header re-resolution stops clobbering the original X-CLIENT-TYPE.

* fix(core): propagate parent distinctId/sessionId to delegated agents

Delegated agents (spawned sub-agents, configured agents, teammates) were
built without distinctId and sessionId, so their Langfuse traces had no
userId or sessionId and did not group with the parent user or session.
Thread the host-resolved distinctId through RuntimeBuilderInput and the
root sessionId through the delegated-agent config provider, and copy both
onto the delegated AgentConfig.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* ci(vscode): make combined nightly manual-dispatch only

The PublishNightly environment gained required reviewers, so each cron
run parked on approval, held the workflow's concurrency group, and
silently cancelled every scheduled run queued behind it. 20 consecutive
scheduled nightlies died this way between 2026-07-31 and 2026-08-21;
the only nightlies that shipped in that window were manual dispatches.

Drop the cron rather than leave a trigger that cannot succeed unattended.

* feat(hub): add drain and upgrade commands with replay support (#13468)

* feat(hub): add drain and upgrade commands with replay support

* handles disconnection

* feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport

Completes the wiring the previous commits' primitives needed:
HubServerTransport gains isDraining(), hub.drain/hub.status/profile.get
command handling, and replayEventsAfter() (backed by the durable event
log), plus the sequence/sinceSequence wire types they depend on in
shared/hub.ts. run-queue-handlers.ts reads the active bot profile's
plugin roots when executing durable runs.

Also adds hub/profiles/: profile.json (identity/rules/plugins) ->
system prompt composition, --profile / CLINE_HUB_BOT_PROFILE
resolution, and the bundled cline-dad profile with its
cline_hub_support read-only diagnostics tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Revert "feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport"

This reverts commit 6696d5d202.

* fix(hub): dedupe replayed events by eventId, not just sequence

HubEventLogStore.append() returns a new envelope stamped with a
sequence rather than mutating the input, so a pending approval
re-issued sequence-less by subscribe() (it predates any durable-log
append) and its later sequence-stamped copy from the durable log are
two different objects carrying the same eventId. The replay-then-live
buffer in browser-websocket.ts only deduped by sequence, so the
sequence-less copy's guard never tripped and it was delivered a second
time when the buffer flushed after replay.

Track delivered eventIds alongside the sequence cursor; eventId
survives the append/stamp round-trip unchanged, so this dedupes the
exact-same logical event regardless of which copy arrives first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): wire drain, durable event log, and run queue into the live transport

CI on this branch failed bun run build:sdk: browser-websocket.ts,
client/index.ts, and hub-websocket-server.ts (already on this branch)
reference sequence/sinceSequence, HubServerTransport.isDraining(), and
the "hub.drain" command — but the commit that reverted bot profiles
out of this branch also reverted this wiring, since it shared a commit
with the profiles work. That wiring is a hub concern, not a
bot-profiles one; split it back out.

- shared/hub.ts: sequence/sinceSequence types, run.enqueue/run.list/
  hub.drain/hub.status/stream.replay capability, command, and event
  names. profile.get intentionally excluded — stays bot-profiles-only.
- context.ts: isDraining() on HubTransportContext. botProfile field
  intentionally excluded.
- hub-server-transport.ts: eventLog/runQueue fields and start/stop
  lifecycle, publish() appends to the durable log, handleCommand cases
  for run.enqueue/run.list/hub.drain/hub.status, drain-refusal check,
  replayEventsAfter()/lastEventSequence(). startBotProfile()/
  startHubSupportTool() and the profile.get case intentionally
  excluded.
- run-queue-handlers.ts: added without handleProfileGet (needs
  ctx.botProfile, which doesn't exist here).
- hub-upgrades.test.ts: added without its two bot-profile-injection
  tests (they need a resolved bot profile to assert against).

Verified bun run build:sdk exits 0 (the exact CI command) and
bunx vitest run src/hub passes (311/312; the one failure is the
same pre-existing environment-timing flake already present before
this change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): export instance-lock, event-log, and run-queue from the hub barrel

These landed as internal modules only; hub-server-transport.ts and
hub-websocket-server.ts import them by direct path, but nothing
re-exported them from the public @cline/core/hub surface the way
sibling discovery/server modules already are.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): wire the instance lock into the daemon entry point

The singleton lock (discovery/instance-lock.ts) and its consumption in
startHubWebSocketServer/ensureHubWebSocketServer were already on this
branch, but the daemon entry point's own half was not: retrying a bind
when a retiring predecessor still holds the lock, and exiting with a
distinct code (3) instead of the generic fatal path when a live Hub
already owns the data directory. Without this, a daemon racing a
retiring predecessor could fail outright instead of waiting the lock
out, and losing the singleton race looked identical to a crash.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): address drain/upgrade review findings (#13478)

- cline hub upgrade: check idleness at least once (--wait 0 works), reject
  non-numeric --wait, and un-drain on every abort path so an aborted
  upgrade can never leave the hub refusing new work
- add cline hub drain --off and the off query param to requestHubDrain so
  POST /drain?off is reachable from shipped code
- HubEventLogStore/HubRunQueue: WAL journal mode + busy_timeout, and stamp
  sequences from lastInsertRowid instead of SELECT MAX(sequence)
- HubInstanceLock.acquire: degrade to an unheld lock when SQLite is
  unavailable instead of refusing hub startup; only BUSY/LOCKED still
  raises HubLockHeldError
- ensureHubWebSocketServer: retire an unusable discovered hub through the
  shared retireDiscoveredHub (busy hubs are attached to, drain precedes
  shutdown, discovery cleared only when the hub actually retired)
- replay adapter: advance the cursor past eventId-deduped events, cap
  replay pages, stop when the cursor stalls, and drop the dedupe set after
  the buffered flush so it cannot grow for the socket lifetime

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* fix(hub): derive the singleton e2e challenger cwd portably

The challenger's working directory was derived by round-tripping the
discovery path through a file: URL and stripping the last pathname
segment. On Windows that yields a POSIX-style '/C:/...' path, which is
not a valid spawn cwd, so the spawn fails ENOENT before the singleton
lock is ever contested and the Windows SDK test job goes red.

The data dir is simply the discovery file's parent: use dirname().

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop stored capability lists from silently revoking tool calling for custom models (#13476)

* fix(core): seed tools capability when custom model capabilities are synthesized from boolean flags

For a models.json entry with no explicit capabilities list, toStoredModelInfo
synthesized a capability array purely from boolean convenience flags (e.g.
supportsReasoning: true -> ["reasoning"]). modelSupportsToolCalling fails open
only for a missing or empty list, so the synthesized non-empty list read as an
authoritative denial and silently stripped every tool definition from requests
to custom OpenAI-compatible models (#13463).

Seed "tools" whenever the list was not explicitly authored and the boolean
projections made it non-empty, preserving the fail-open contract. Explicitly
authored capability lists remain authoritative and can still disable tools.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(core): cover stale catalog capability overrides

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: treat stored capability lists as non-authoritative for tool calling

The hasExplicitCapabilities guard still let two producers of tool-less
lists through:

- The VS Code legacy-override migration (legacyModelInfoToOverrides)
  persists explicit partial lists like ["prompt-cache"] into models.json
  for custom OpenAI-compatible models, which then read as an authoritative
  "cannot call tools" and drop every tool - same symptom as #13463.
- Any hand- or UI-authored partial list on a non-catalog model.

Stored entries and user-authored provider metadata have no way to declare
"cannot call tools" (there is no supportsTools field, and every writer
that authors a full list includes "tools"), so seed "tools" into any
non-empty list for a language model. Only generated catalog capabilities
remain authoritative - a genuine no-tools catalog model stays that way -
and non-language models (e.g. image generation) never gain a tools claim.

Also make legacyModelInfoToOverrides write "tools" into the arrays it
fabricates, matching the providers.json migration, so models.json stops
being poisoned for older readers.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): prepare 4.1.13 release

* chore(sdk): release v0.0.78

* chore(cli): release v3.0.57

* fix(core): run hub e2e files serially so daemon timing budgets survive CI contention

singleton.e2e.test.ts (added in #13468) spawns real daemons and runs for
~15s. Vitest's default file parallelism let it run alongside
shutdown.e2e.test.ts, whose assertions are wall-clock bound: discovery
within 10s, exit within 5s, and a 2s shutdown watchdog. On the 2-core
windows-latest runner that contention alone broke those budgets, failing
the shutdown test two different ways across runs — once never observing
discovery, once with the daemon forced to exit before its HTTP 202
flushed (socket hang up). The test passed on Windows before #13468 and
has failed every SDK publish run since.

* chore(desktop): release v0.0.16

* test(sdk): give windows-sensitive suites realistic timeouts

Four consecutive SDK publish runs failed on windows-latest, each on a
different test, all of them plain timeouts: two @cline/shared SQLite
tests at the 5s vitest default, core's bash executor at 10s, and the hub
singleton endpoint test at 10s. The 2-core Windows runner spawns forks
and takes SQLite locks slowly enough to blow those budgets under load.

These timeouts guard against hangs; they are not timing assertions (the
one suite that does assert elapsed time, shutdown.e2e, was fixed by
removing file-level parallelism instead). Raise core to 20s and give
@cline/shared an explicit 15s in place of the inherited 5s default.

* fix(telemetry): emit task.completed from every session teardown path (#13489)

The task.completed fallback lived only inside shutdownSession, but
stopSession/dispose route interactive sessions with a terminal reported
status through releaseSessionRuntime, which never emitted. Truthful
session-status reporting (shipped in 4.1.11) re-routed a large share of
interactive stops onto that branch and silently dropped the event.

Route the emission through a single choke point,
emitTaskCompletedOnTeardown, called from both shutdownSession and
releaseSessionRuntime. The completion criterion no longer reads
session.status: interactive sessions use the recorded final-turn
outcome (lastInteractiveTurnFinishReason), non-interactive sessions
keep the existing input.status === "completed" logic. A new
taskCompletedEmitted flag (also set by the submit_and_exit observer)
enforces exactly one task.completed per session. failSession now
records the errored final turn so a stale "completed" from an earlier
turn can never leak into the teardown emission. Telemetry only; no
user-facing behavior changes.

* chore(vscode): release v4.1.14

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on (#13498)

* fix(vscode): honor MCP auto-approve settings for SDK tool calls

The SDK extension required both the global 'Use MCP servers' auto-approve
toggle AND each tool's per-tool autoApprove flag before silently approving
an MCP call, while the legacy extension treated them as either/or. Restore
the legacy OR semantics so toggling MCP auto-approve works again.

Also key toolPolicies by the registered SDK tool name (via
defaultMcpToolNameTransform, now exported from @cline/core) instead of raw
server__tool. Servers whose names contain sanitized characters (e.g.
marketplace names like github.com/user/repo) or exceed 64 chars produced
policy keys that never matched the registered tool, so those MCP tools ran
without any approval gate; the live auto-approve lookup now re-applies the
transform instead of string-splitting the name.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Revert "fix(vscode): honor MCP auto-approve settings for SDK tool calls"

This reverts commit 86c568fbba.

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on

The SDK extension only auto-approved an MCP call when the global 'Use MCP
servers' auto-approve toggle AND that tool's per-tool autoApprove flag were
both set, so toggling MCP auto-approve appeared to do nothing and users had
to opt in each tool individually. The toggle alone now governs all MCP
tools; the per-tool flag is no longer consulted.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): release v4.1.15

* fix(cli): remove the $4.99 ClinePass promo copy (#13514)

The $4.99 first-month promo is ending, so the CLI's first-launch "Try ClinePass" dialog should no longer advertise it. Also drops the leftover CLI_PROMO_CODE plumbing, which has been an empty string since the promo-code flow was removed.

* fix(vscode): resolve hook workspace identity from the window, not shared global state (#13352)

* fix(vscode): resolve hook workspace identity from the window, not shared global state

Hook discovery, hook cwd selection, and the workspaceRoots metadata passed
to hook scripts all read the workspaceRoots/primaryRootIndex global state
keys. Global state lives in ~/.cline and is shared by every Cline instance
(all VS Code windows, the CLI, the JetBrains plugin), and nothing writes
these keys anymore, so hooks resolved against whatever project some other
or older instance last recorded. With a second window open on another
project, a workspace's .clinerules/hooks scripts were never discovered.

Resolve workspace roots via a single guarded helper backed by
HostProvider.workspace.getWorkspacePaths() (in-process, window-scoped,
same as refreshHooks): blank paths are filtered, a host-bridge failure
degrades to no workspace roots instead of silently disabling global hooks
or skipping blocking PreToolUse guards, and one resolution is threaded
through hooks-dir discovery, cache misses, cwd selection, and hook input
metadata so they can't disagree (previously up to four host lookups per
hook execution — real gRPC round trips in the standalone host). Roots and
hooks dirs are matched on whole path segments with the longest root
winning, so prefix-sharing or nested workspace roots resolve to the right
project. The adapter creates the runner once per event and skips no-op
runners, making creation the single resolution point; the separate
hasHook/getHookInfo checks are removed. The dead workspaceRoots and
primaryRootIndex state keys are dropped, and the four hand-rolled
HostProvider.workspace test stubs are consolidated into one shared
helper.

* test(vscode): add e2e coverage for workspace-scoped hook discovery

Boots real VS Code with the packaged extension against the workspace
fixture, sends a prompt, and asserts the fixture's UserPromptSubmit hook
was discovered from the open window's workspace, executed with that
workspace root as its cwd, and received the same root in its
workspaceRoots input — the end-to-end contract the hook workspace
identity fix establishes.

* test(vscode): isolate the e2e hook fixture from the shared workspace

The UserPromptSubmit fixture hook lived in the shared e2e workspace, so
every prompt-sending spec executed it (hooksEnabled defaults to true) —
and its cold PowerShell spawn on Windows pushed chat.test.ts past the
5s expect timeout. hooks.test.ts now overrides workspaceDir to a
dedicated workspace-hooks fixture, so only the hooks spec pays the hook
spawn.

* fix(hub): cap hub-events db size so it can't fill the disk (#13516)

* fix(hub): cap hub-events db size so it can't fill the disk

Row/time retention alone didn't bound disk usage: envelopes carrying
full session snapshots reach hundreds of KB each, so retained rows
could total tens of GB, sweeps only ran hourly, and DELETE never
shrinks a SQLite file. Enforce a 64 MiB size budget in prune() (oldest
rows first, VACUUM to return the space), and also prune after every
16 MiB appended so bursts can't outrun the hourly timer.

Fixes #13505

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): tolerate VACUUM failure on a full disk

VACUUM needs scratch space and can fail in exactly the state a
ballooned event log causes. The byte-budget deletes already bound live
data, so swallow the error and let the next sweep retry the reclaim
instead of aborting startup pruning and disabling the durable log.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): count the size budget in UTF-8 bytes, not characters

envelopeJson.length (UTF-16 units) and SQLite LENGTH() (characters)
undercount multibyte text by up to 3x, which could leave a CJK-heavy
log settled above budget and re-running VACUUM every sweep. Use
Buffer.byteLength and LENGTH(CAST(... AS BLOB)) instead.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(sdk): carry root overrides into the Node smoke-test sandbox (#13517)

ci-node-smoke.ts installs the packed SDK tarballs with a plain npm
install in a fresh temp dir, where the repo root package.json overrides
do not apply. When @sap-cloud-sdk 4.9.0 shipped (2026-08-24) it broke
@sap-ai-sdk/ai-api 2.14.0 (via @jerome-benoit/sap-ai-provider in
@cline/llms) with ERR_PACKAGE_PATH_NOT_EXPORTED, failing the smoke step
on every PR even though the root already pins @sap-cloud-sdk/* to 4.6.0.

Copy the root overrides block into the generated sandbox package.json
so the smoke install resolves the same pinned versions as the repo and
future third-party releases cannot break it independently.

* chore(sdk): release v0.0.79

* fix(vscode): don't steal last-used provider from ClinePass on credential refresh (#13520)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): flush the /shutdown 202 before daemon teardown

The /shutdown handler queued teardown on a microtask, which runs before
the event loop's write phase, so the daemon could process.exit() before
the accepted 202 was handed to the socket. Unix masked it (uv_try_write
lands small loopback writes synchronously); Windows has no such fast
path and lost the race regularly — the recurring shutdown.e2e.test.ts
'socket hang up' failures on windows-latest. Start teardown from the
response's write callback instead, with an idempotent 1s fallback so a
client that vanishes mid-write cannot strand the daemon, and send
Connection: close so the client gets a FIN rather than an abort.

Since the flakiness this compensated for is fixed at the source, restore
maxWorkers: 2 for the Windows core suite (serializing it cost ~3 min of
CI per run), and raise the e2e daemon discovery hang guard 10s→30s —
it guards against hangs, not runner speed.

* chore(cli): release v3.0.58

* fix(core): prevent search_codebase from crashing the process on giant single-line files (#13525)

* fix(core): prevent search_codebase from crashing the process on giant single-line files

searchWithRipgrep buffered all of rg's --json stdout into one string. Each
JSON event embeds the full text of the matched line (--max-columns is
ignored in JSON mode), so searching a directory of serialized trace dumps
(single-line multi-hundred-MB JSON files) accumulated gigabytes of stdout
until string concatenation threw RangeError: Out of memory inside the
stream data handler. That throw is outside the tool's try/catch, so it
escalated to an uncaughtException and killed the CLI/hub daemon.

Parse rg's JSON events incrementally line by line, drop events larger
than 256KB, truncate matched/context lines to MAX_LINE_CHARS, and stop
reading once maxResults is reached. The fallback regex scan now skips
files larger than 10MB (reporting the skip count) and truncates its
context lines the same way.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* simplify search_codebase crash fix to a minimal diff

Replace the incremental JSON-event parser with three small guards: stop
buffering rg stdout past 10MB, drop the trailing partial event before
parsing, and slice fallback context lines to MAX_LINE_CHARS. Drops the
fallback file-size skip and skip-count reporting.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag (#13522)

* chore(vscode): remove per-tool MCP auto-approve checkboxes from webview

MCP auto-approval is now governed solely by the global 'Use MCP servers'
toggle; the SDK approval path (shared with the CLI and desktop app) has no
per-tool granularity, so the per-tool and 'Auto-approve all tools'
checkboxes were no-ops that implied control that no longer exists. Remove
them from the MCP settings view and chat tool rows. The autoApprove arrays
in cline_mcp_settings.json and the toggleToolAutoApprove RPC are left
intact for the legacy extension.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag

Keep the checkbox components, handlers, and RPC plumbing intact but gate
rendering behind SHOW_MCP_PER_TOOL_AUTO_APPROVE=false: the SDK approval
path (shared with the CLI and desktop app) is all-or-nothing via the
global 'Use MCP servers' toggle, so the per-tool checkboxes were no-ops.
Flip the flag back on if the SDK gains per-tool approval granularity.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(tools): create new files with the platform-native line ending (#13521)

* fix(tools): use platform-native EOL for new files and preserve CRLF in apply_patch updates

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* simplify to the minimal new-file EOL fix

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* extract shared normalizeNewFileLineEndings helper

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add suggested schedule templates to the desktop Schedules page (#13529)

* Add suggested schedule templates to desktop Schedules page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix unreadable selected text in inputs caused by selection utility conflict

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Restyle Suggested section label as small gray uppercase

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Hide suggested schedule cards that match an existing schedule name

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Disable the agent todo tool and hide the Agenda UI in the desktop app (#13530)

* remove todo tool and Agenda UI, keep schedule-only tasks tool

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: biome formatting fixes

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* restore agenda backend; disable todo kind behind a flag instead of deleting

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* keep agenda automation pump idle while the todo tool is disabled

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* remove todo tool and Agenda UI altogether (revert the disable-flag hybrid)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* restore all agenda code to main state

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* disable agent todo tool and hide Agenda UI behind flags

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add Desktop App and Cloud Platform to bug report issue template (#13532)

* Add Desktop App and Cloud Platform to bug report surfaces

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename Surface Diagnostics field to Diagnostics

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: sidebar navigation cleanup with New/Schedule/Customize rows and dialog-based search (#13533)

* desktop: clean up sidebar navigation chrome

- Give New Task its own full-width labeled row below the logo row
  instead of an ambiguous icon next to the agenda toggle
- Wire the New Task row to the home action so starting a new task
  clearly takes you home (the logo still works as a fallback)
- Swap back/forward chevrons for browser-style arrow icons and
  bump their size

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: sidebar New/Schedule/Customize rows and always-visible search

- Stack New (plus icon), Schedule, and Customize as full-width labeled
  rows below the logo; whole row highlights on hover via sidebarItem
- New starts a fresh task (home), Schedule opens Settings > Schedules,
  Customize opens the Customizations sections (Plugins first)
- Show the session search bar permanently above the sessions list
  instead of hiding it behind a search icon toggle

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: move session search into a dialog behind a logo-row icon

- Replace the inline sidebar search bar with a search icon in the
  logo row that opens a cmdk command dialog listing sessions
- Selecting a result opens that session and closes the dialog
- Remove the agenda/tasks toggle the icon replaces, along with the
  now-unreachable sidebar Agenda panel (the welcome screen still
  surfaces agenda tasks)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: load full session history when the search dialog opens

Addresses Greptile review on #13533: the dialog only searched the
currently loaded history batch, so older unloaded sessions could not
be found. Opening search now kicks off loadAllSessions() (the hook's
purpose-built global-search loader), and the empty state reads
'Searching older sessions...' while more history is streaming in.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Hide Channels and Agents sections from desktop app sidebar (#13527)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop app: organize sidebar sessions into Pinned, Scheduled, and Tasks sections (#13528)

* Add Pinned/Scheduled/Tasks categories to desktop app sidebar

Replace the Schedules and Favorites filter-menu options with visible
collapsible category sections in the session sidebar, and rename the
Favorite action to Pin across the sidebar and sessions view.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Grow full history window when Tasks show-more outpaces loaded tasks

loadMoreSessions treats its argument as a limit on all sessions, but the
Tasks show-more count only tracks Task rows, so once pinned/scheduled
rows pushed the loaded total past the requested count the call no-oped
and clicks went dead. Grow the whole history window via
loadOlderSessions instead, and only when the loaded tasks cannot fill
the next page. Addresses Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Auto-fill the Tasks page instead of fetching once per show-more click

A single 50-session window growth can consist entirely of pinned or
scheduled sessions, leaving a show-more click with no visible Tasks
progress. Replace the one-shot fetch with a page-fill effect that keeps
growing the history window until the requested Tasks page fills or
history runs out. Addresses the follow-up Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Halt page-fill retries after a failed history fetch

A failed fetch leaves the task count and has-more state unchanged,
which are exactly the conditions the page-fill effect fires on, so one
failing request would retry and re-toast forever. Halt the effect after
a failure and let the next explicit show-more click retry. Addresses
the third Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Redesign desktop Model Providers page and split voice input into its own settings page (#13531)

* Redesign desktop Model Providers page and split voice input into its own settings page

- Group providers into Connected / Popular / All with auth-kind hints and
  connection status instead of per-row enable toggles
- Show browser sign-in (not an API key field) for OAuth providers, with a
  collapsed manual-key escape hatch where supported, plus explicit
  Connect / Disconnect / Sign out actions
- Move voice input to a dedicated Settings > Voice page that only offers
  connected transcription-capable providers, preselects a default model
  (streaming preferred), and stays disabled in the sidebar until a
  provider is connected

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Show native tooltip on the disabled Voice settings nav item

Disabled buttons drop pointer events, so the 'connect a model provider'
hint moves to a wrapping span for the browser tooltip to render.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Drop letter avatars and gray provider ids from provider rows and voice chips

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Drop model counts from provider list rows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename provider Connected status to Configured and drop the green styling

A settings entry is configuration, not a live connection; neutral gray
text avoids implying an active link, since the user still picks which
configured provider to use per chat.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Resync provider catalog from disk when a settings save fails

Connect/disconnect/credential edits update the list optimistically; a
failed save now reloads the catalog instead of leaving the optimistic
state (and the view's module cache) claiming a configuration that was
never persisted.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename oauthProvider test fixture to dodge CodeQL name heuristic

CodeQL's clear-text-storage query flags any identifier matching 'oauth'
as a credential source and traced the fixture's provider id into the
favorite-models localStorage write, which stores only provider/model id
strings. Renaming the fixture removes the false-positive source.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Guard catalog reloads against races and resync detail drafts on failed saves

Optimistic provider mutations now bump a generation that discards any
in-flight catalog response, so a failed-save recovery reload can't
overwrite a newer edit with an older disk snapshot. The recovery also
remounts the provider detail panel via a reset token so its local field
drafts reflect the reloaded on-disk state instead of unpersisted edits
or an optimistically cleared disconnect.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix failed-save recovery ordering and retry superseded reloads

Remount the provider detail only after the authoritative catalog reload
lands, so its drafts re-seed from disk state rather than the optimistic
values that failed to persist. When a concurrent edit supersedes the
recovery's in-flight response, retry the reload (bounded) instead of
dropping it, since that edit performs no reload of its own.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): Customize hub, sidebar overhaul, and settings polish (#13538)

* feat(desktop): merge customization pages into a Customize hub with inline marketplace

Replaces the Plugins page and the dedicated Marketplace page with a single
Customize hub. Tabs: Skills, MCP, Plugins, Rules, Hooks, Tools, each with
live counts. Tabs backed by a marketplace catalog render the installed
items followed by an inline browsable Browse section (CLI-hub style), so
installing from the catalog immediately reflects in Installed above.

- Installed cards restyled to mirror the browse-card anatomy: bg-card p-4
  containers, absolute top-right xs Uninstall matching Install, truncating
  semibold titles, primary-tinted icons, real Badge components instead of
  ad-hoc bordered spans, un-indented line-clamped descriptions
- Rules/Hooks/Tools rows brought into the same card language; redundant
  intro paragraphs (duplicating the page description) removed; Tools group
  headers match the Installed header style with counts
- Marketplace section header renamed to Browse; duplicate 'N results' row
  removed (the header count is the single source)
- MCP embedded view now shows the full marketplace instead of
  installed-only

* feat(desktop): overhaul sidebar sessions and navigation

Sessions list:
- Sort toggle removed; sessions are always grouped by project, with pinned
  sessions leading each group (both subsets ordered by recency). The
  Pinned/Scheduled/Tasks category sections and their time-mode paging
  machinery (page-fill effect included) are deleted
- Scheduled sessions get an inline clock icon next to the pin position;
  pin + clock render together when both apply, and the running/unread
  status dot now coexists with them
- One font size (text-sm) across the list: titles, timestamps, project
  headers, show-more buttons, empty states. sidebarText needed !text-sm
  because the default button size's text-base wins the twMerge conflict
- Gradient fade under the Sessions header once the list scrolls, so rows
  fade out instead of hard-clipping
- The session-detail hover card is controlled from the sidebar and closes
  on scroll (Radix receives no pointer events while scrolling, so it used
  to float over moving content)
- Sidebar min resize width raised 224->260px; the per-project show-more
  label truncates so its nowrap text can't force rows to overflow and clip
  timestamps at narrow widths

Navigation:
- Customize replaces the Plugins/Marketplace/Hooks/Rules/Tools sidebar
  entries; Schedules and Customize are hidden from the expanded settings
  nav (their top rows cover them) but stay reachable when collapsed
- The settings gear always opens General instead of resuming the last
  section; the Account no-op hover special case is gone
- The New row highlights (aria-current) while the fresh not-yet-started
  task page is showing and hands off to the session row once the task
  starts; hitting New also focuses the prompt input via a window-event
  signal (lib/prompt-input-focus.ts) since the sidebar and composer sit in
  distant subtrees
- Fixed the xs button size collapsing any icon-bearing button to 12x12
  (leftover has-[>svg]:size-3 from when xs was a micro button) — this was
  why Uninstall buttons rendered broken next to Install

* feat(desktop): polish settings pages and chat composer

Models page:
- The provider detail panel is always open: no X button, no empty
  no-selection state. It defaults to the first connected provider (falling
  back to the first in the catalog), which also removes the layout shift
  that happened when the page swapped between full-width and panel
  variants on selection
- Fixed the list pane becoming unscrollable while the panel was open:
  grid items default to min-size auto, so the pane grew past its track
  inside the overflow-hidden grid and its ScrollArea had nothing to
  scroll; wrapped it in a min-h-0 min-w-0 cell
- Add Provider opens a Dialog instead of swapping the page
  (AddProviderContent gained a dialog variant that renders only the form)
- Embedded inputs (provider search, model search, detail fields) share one
  EMBEDDED_INPUT_CLASS stripping the Input component's own border/dark bg
  tint/shadow/ring, which rendered as a mismatched inner box; the model
  search box uses the same h-9/px-3 frame as the provider search
- Model list flows with the page instead of a max-h capped inner scroller

Other pages:
- Account uses the shared PageFrame/PageHeader: left-aligned, text-3xl
  title, Sign Out in the header actions slot
- Desktop notifications is one General section: header row plus the
  Event/Notify/Sound matrix nested in a card, so its rows no longer read
  as top-level peers of Dark mode; 'Available in the desktop app' label
  removed
- Schedule page retitled from Schedules with a real description; Customize
  description rewritten

Chat composer:
- The voice dictation button only renders once a voice model is
  configured (Settings -> Voice); the unconfigured deep-link state is
  gone (prop type kept for an easy restore)

* chore(desktop): release v0.0.17

* fix(desktop): unblock sdk-test lint on the voice-input model picker (#13553)

The model picker renders a radiogroup of styled buttons with role=radio
and aria-checked; biome's useSemanticElements flags the role as an
error, which fails the sdk-test Quality Checks lint for every PR
touching sdk/ or apps/ paths. Suppress with a justification — switching
to input type=radio needs a restyle and belongs to the desktop settings
work.

* fix(vscode): include rich workspace metadata in system prompt (#13518)

* capture richer workspace information for vs code extension

* fix(shared): redact credentials from workspace remotes

* fix(shared): avoid regex backtracking in remote redaction

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>

* Hide task costs on vscode when ClinePass is selected (#13515)

* fix: stop showing cost estimates for subscription-billed providers (#13552)

* fix(vscode): stop showing cost estimates for subscription-billed providers

Providers whose usage is covered by a flat-rate subscription (ChatGPT
Plus/Pro via openai-codex, ClinePass) are marked with
metadata.usageCostDisplay = "subscription" in the SDK, and the CLI
already suppresses dollar figures for them. The VS Code host collapsed
that value into "show" before it reached the webview, so the task
header and model pricing rows rendered API-rate cost estimates that
users read as real charges on top of their subscription.

Pass all three usageCostDisplay values ("show" | "hide" |
"subscription") through the catalog listing and render cost only when
the value is "show", matching the CLI's shouldShowCliUsageCost
policy.

* feat(llms): mark Claude Code as a subscription-billed provider

Claude Code is typically authenticated with a Claude Pro/Max
subscription, but its models reuse Anthropic API pricing metadata, so
Cline rendered per-token prices and API-rate cost estimates for usage
that is covered by the subscription. Set usageCostDisplay =
"subscription" on the provider (picked up by the CLI and the VS Code
webview) and suppress the price rows in the Claude Code settings card.

The Claude Code CLI can also run on API-key billing, where a real cost
exists; the provider cannot distinguish the two, so we prefer showing
no number over a misleading one.

* fix(vscode): suppress cost display until provider listings load

While the ListProviders request is in flight (or after it fails), the
usage-cost hook had no listing to consult and fell back to "show",
flashing the API-rate estimate at subscription users on every chat-view
mount — the exact display the previous commit removes. Return
"unknown" whenever listings are absent; consumers already render cost
only for "show", so they suppress it during that window with no
changes. Briefly hiding a real cost is harmless, briefly showing a fake
charge is not.

* fix(desktop): reconcile voice settings after main sync

* test(llms): allow experimental ElevenLabs models

* fix(sdk): preserve canonical media model behavior

* feat(desktop): customize macOS DMG install window (#13563)

* feat(desktop): add Retina DMG background tooling

* feat(desktop): customize the macOS DMG layout

* ci(desktop): validate DMG background assets

* fix(desktop): adjust DMG Applications icon position

* ci(desktop): drop redundant DMG artwork validation from publish workflow

Tauri's beforeBuildCommand already runs dmg:background (with its own
validation) at the start of the build/sign/notarize step, and the
release/beta config overlays do not override the build section, so this
step duplicated work the publish job performs anyway. PR-time coverage
lives in desktop-test.yml.

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>

* feat(desktop): sidebar time view, Customize/Marketplace split, and schedule page UX (#13570)

* feat(desktop): split Customize into Installed and Marketplace pages

The Customize hub previously embedded a Browse section inside every tab
that had a catalog. That inlining made each tab long and buried the
catalog. Customize is now the installed inventory only (skills, MCP,
plugins, rules, hooks, tools tabs pass marketplaceVariant="installed"
to the embedded MarketplaceView; McpServersContent grew the same prop),
with an outline Marketplace button in the header.

Browsing moved to a dedicated Marketplace settings section that renders
the previously dead "directory" variant of MarketplaceView: one list
across all catalog types with type-filter chips, wrapping tag chips,
and light rules separating the filter tiers from each other and from
the results. The Clear control now renders inline at the end of the tag
row only while a tag is active, the Updated date is gone, and the
header hosts an Installed button mirroring the one on the Customize
page. Directory subheader copy: "A curated set of plugins, MCP
servers, and skills from the Cline community."

Tag and type chips wrap to new lines instead of scrolling
horizontally.

* feat(desktop): sidebar time view with sections, sort toggle, and scheduled detection

Restores the time-sorted session list as the default sidebar view, with
collapsible Pinned / Scheduled / Tasks sections (headers appear only
once something is pinned or scheduled) and the page-fill effect that
grows the fetched history window until a Show-more click makes visible
progress. Project grouping stays as the alternate mode behind a
one-click sort toggle whose icon reflects the active mode — the old
dropdown cost an extra click for a two-option choice.

Scheduled sessions are detected two ways: the hub-schedule origin
trigger in session metadata, plus a fallback that asks the hub which
session ids belong to schedule executions (list_routine_schedules,
fetched on mount and every two minutes, merged into a rolling set).
The fallback matters because locally executed scheduled runs do not
reliably stamp the trigger into session metadata — a real scheduled
session created today carried only {mode:"user"} provenance. The
scheduled clock icon now leads the row, left of the title; pin and
timestamp stay on the right.

The initial visible page grows from 10 to 30 rows so a tall sidebar
fills instead of stranding a stub of rows over empty space (history
fetches already start at 50).

The expanded sidebar's Customize row now hosts indented Installed and
Marketplace sub-tabs while a customize section is open; the active
sub-tab carries the full selected background while the parent keeps a
subtler one so the two simultaneous highlights read differently.

Also fixes the hover-card flash on click (logo card and session-row
cards): Radix HoverCardContent sits on a DismissableLayer, so a click
on the trigger registers as a pointer-down outside the card and
dismisses it, and the trigger's focus event immediately reopens it.
onPointerDownOutside preventDefault suppresses the dismissal; cards
still close on pointer leave.

* feat(desktop): schedule page row, dialog, and details UX polish

Schedule cards are now click targets: clicking anywhere on a card
outside its controls opens the details dialog (guarded via
closest("button,...") since every inline control, including the Radix
switch, renders a button element), with Enter/Space keyboard support.
The redundant eye button is gone. The remaining edit / run / pause /
delete buttons grow from the 12px icon-sm size to 28px targets with
16px icons, sized consistently with the adjacent enable toggle — the
icons use explicit size-4 classes so the Button base svg rule cannot
shrink them back.

The new/edit dialog gains breathing room between field labels and their
inputs (space-y-2 per field wrapper).

The details dialog no longer scrolls as a whole when the schedule JSON
is long: the dialog is a flex column capped at 85vh, the JSON pre
shrinks to the remaining space (min-h-0) and scrolls internally, and
the Runs tab list scrolls inside the tab the same way.

* feat(desktop): scheduled sessions UX — unified details dialog, run-now handoff, hidden steering, stuck-thinking fix (#13573)

* feat(desktop): merge schedule details into one view and open run-now sessions

The schedule details dialog drops its Overview/Runs tabs: one scrollable
column with the meta grid, the configuration JSON (capped at max-h-64
with internal scroll so it cannot crowd out what follows), and a Runs
section beneath it showing the three most recent runs with a ghost
"Show all N runs" expander (collapsed again whenever a different
schedule's details open). The "Full configuration for this schedule"
subtext is gone; the dialog passes aria-describedby={undefined} so
Radix does not warn about the missing description.

Run now hands you into the session it starts. The trigger command
queues the run and returns before the runner attaches a session id, so
after the toast the handler polls the schedule overview once a second
for up to 15 seconds — which doubles as keeping the page's run status
fresh (refreshSchedules now returns the fetched overview to make that
single-stream) — until the triggered execution reports its session id,
then calls onOpenSession. Guarded so it never auto-navigates after the
user left the page.

* feat(desktop): hide runtime steering messages from transcripts

Scheduled/automation runs inject user-role steering messages each
iteration ("[SYSTEM] This run is not complete until you call
submit_and_exit...", plus a team-obligations variant). The chat view
rendered them as user bubbles, as if the person had typed them — in a
scheduled session the transcript was mostly [SYSTEM] noise.

They are machinery talking to the model, not something the person said
or needs to read, so the transcript now hides them entirely:
MessageBubble renders null for any [SYSTEM]-prefixed user message.
Grouping still treats them as working-row machinery via a single
isSystemSteeringMessage predicate — they collapse into the run's work
span, are never a turn boundary, can never be mistaken for a run's
answer, and never advance the run count even when metadata is missing —
so work-block folding and checkpoint/edit run numbering stay correct.
A finished scheduled session now reads as prompt, work summary, answer.

* fix(desktop): poll history while an attached session's event stream is dead

Opening a scheduled session while (or right after) it runs left the
view stuck on the thinking shimmer until the user switched away and
back. Root cause is in core: the hub daemon executes scheduled runs on
a private LocalRuntimeHost inside createLocalHubScheduleRuntimeHandlers,
while the hub server only projects live events from its own session
host — so session.attach succeeds but no assistant/tool/status events
ever flow. And since multiple hub daemons share cron.db, a run claimed
by a different daemon is invisible to this hub regardless. The proper
core rewiring is tracked as ENG-2474.

Client-side heal that covers every case: while an attached history
session reports a busy status and no chat_event chunk has arrived for
five seconds (and no assistant bubble is mid-stream), poll every three
seconds — re-read canonical history, merged through the same dedupe
path hydration uses, and the session record's status — so the
transcript and the thinking indicator settle in place. Locally driven
turns keep chunks flowing, so the quiet-window guard keeps the fallback
inert there.

* chore(desktop): format workspace selector components

Biome formatting drift that landed on main; picked up by a formatter
pass over components/views/chat.

* fix(desktop): keep stale-stream poll inert during locally driven turns

The fallback poll could fire between a local submit and the model's
first chunk (optimistic user bubble added, stream quiet past the
window, no assistant bubble yet). It then replaced the optimistic
bubble — raw prompt text — with its canonical history twin, which is
stored wrapped in a user_input envelope. The rekey handler that runs
when the stream starts looks for a trailing user bubble matching the
raw prompt, finds only the wrapped copy, and appends a second bubble:
duplicated messages in normal interactive chat.

The poll now stays inert while a local turn is in flight
(turnEpoch !== turnSettledEpoch, or outstanding optimistic user
messages), checked both before polling and again after the snapshot
returns. Hydration marks the turn settled — the mount defaults
(epoch 0, settled -1) otherwise read as an open turn and would keep
the fallback inert forever for the scheduled-session case it exists
for. Applying a polled snapshot also rebuilds the live tool routing
keys, same as hydration, so later tool events update canonical rows
in place instead of appending.

* fix(desktop): keep the working indicator alive for narrating scheduled runs

Watching a scheduled run live: the first tool row appeared, then the
thinking indicator vanished with nothing streaming, and the rest of
the run (final answer, submit_and_exit) only showed up seconds later
in one lump.

inferHydratedChatStatus treats a "running" session record whose
transcript ends on an assistant message as a session that died without
a status flip and reports "completed". That heuristic is right for
stale records, but scheduled/automation models narrate between tool
calls, so a polled snapshot can genuinely end on assistant text
mid-run — the completed flip hid the working indicator, folded the
run early, and disarmed the stale-stream poll (status left the busy
set), dead-ending live updates until an in-flight poll happened to
deliver the finished run.

The heuristic now only applies once the transcript has actually gone
quiet (newest message older than two minutes — comfortably past model
latency plus tool runs). A recently active transcript keeps the
record's "running" verdict, so the indicator stays up and polling
stays armed until the record itself settles.

* fix(desktop): stale-stream poll mirrors the session record instead of inferring

Replaces the previous fix for the vanishing working indicator (the
time-window guard added to inferHydratedChatStatus) with a version
that adds no inference at all: the heuristic is restored to exactly
its long-standing form, and the poll now maps the session record's
status verbatim (mapSessionRecordStatus).

The record is the right authority in the poll's context: the sessions
this fallback serves have a live host maintaining their record, and it
flips to a terminal status when the run ends. Transcript-shape
inference belongs only where it has always lived — hydrating sessions
whose records may be orphaned — and would misread a mid-run snapshot
ending on assistant narration as a finished session, hiding the
working indicator and disarming the poll.

* fix(desktop): address review findings on steering detection and run-now matching

Steering detection additionally requires the injected-message marker
(meta.userRunSpan === 0) beside the [SYSTEM] prefix, so a person's
genuine prompt that happens to start with "[SYSTEM]" stays visible
and turn-counted. The failure direction is deliberate: an unstamped
injected reminder would merely show as a user bubble, while the
content-only check could hide a real prompt.

Run-now only follows the execution id the trigger reply itself named;
the newest-execution-for-this-schedule fallback could open a previous
run's session when the trigger failed to enqueue one.

* fix(desktop): report a failed run-now instead of confirming a start

A trigger reply without an execution means no run was enqueued (the
schedule may have been disabled or deleted since the page loaded). The
handler previously toasted "Run started" regardless and then silently
skipped the session-open polling. It now shows a destructive
"Run not started" toast, refreshes the schedule list so the row
reflects reality, and skips the polling entirely.

* fix(desktop): don't block the main thread on quit while stopping the sidecar (#13566)

Quitting the mac app beach-balled for ~5-7s. The shutdown POST was
built from the ws transport URL (appending /shutdown lands inside the
query string), so the sidecar was never told to exit, and stop() then
polled the child for up to 7s on the main thread - on macOS inside
applicationWillTerminate - before SIGKILLing it.

stop() now sends SIGTERM and returns immediately. The sidecar handles
SIGTERM with the same bounded (5s) graceful shutdown as the /shutdown
endpoint and exits itself, finishing session persistence as an orphan.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): hover trash button on sidebar session rows (#13582)

Each session row shows a trash icon on the right while hovered (or
when the button itself is focused), opening the same delete
confirmation dialog the row's context menu uses. The row is a button
and buttons cannot nest, so the trash is an absolutely positioned
sibling inside a group/row wrapper, overlaid where the timestamp sits:
row hover hides the timestamp, shows the trash, and moves the row's
hover background to the wrapper group so it holds while the pointer is
on the trash itself.

* fix(desktop): install marketplace plugins and MCP servers in-process instead of spawning a cline binary (#13585)

* fix: install marketplace plugins and MCP servers in-process instead of spawning a cline binary

The desktop app sidecar and cline-hub shelled out to 'cline plugin install'
and 'cline mcp install' for marketplace installs. Packaged GUI apps inherit
launchd's minimal PATH on macOS and most desktop users have no cline CLI
installed at all, so installs failed with a red
'Executable not found in $PATH: "cline"' error.

Install via @cline/core's installPlugin/installMcpServer in-process instead,
matching what the VS Code extension already does. Also fix
parseMcpInstallArgs in @cline/core to treat the marketplace catalog's '--'
separator as end-of-options; previously the separator itself became the
stdio command.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop test-injection plumbing from marketplace installers

Call @cline/core's installPlugin directly instead of threading an
installer option through the marketplace entry points; tests stub the
core module instead.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert: keep cline-hub marketplace installs CLI-backed

The hub dashboard is launched via 'cline dashboard', so a CLI is always
present and CLINE_WRAPPER_PATH resolves it; the PATH bug only affects
the desktop app, which does not ship a CLI.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.18

* chore(vscode): release v4.1.16

* chore(sdk): release v0.0.80

* chore(cli): release v3.0.59

* fix(hub): stop shipping full transcripts inside broadcast hub events (#13587)

* fix(hub): stop shipping full transcripts inside broadcast hub events

Every session.updated (and session.created/detached/run.started) event
embedded the session's ENTIRE message transcript via readCoreSessionSnapshot,
even though no consumer reads snapshot.messages off an event — clients fetch
messages with the session.messages command. For a multi-megabyte transcript
this turns every status flip into megabytes per subscriber, floods the
durable event log, and (until the send-queue backpressure fix lands) lets a
slow subscriber balloon the hub process by one full transcript copy per
event — reported as a 25GB cline process on a 16GB Mac.

Strip snapshot.messages centrally in HubServerTransport.publish() so every
current and future event publisher is covered, the event log stores slim
envelopes, and cursor replay stays byte-identical with live fan-out. All
other snapshot fields (status, usage, model, workspace, checkpoint) are kept,
and command replies are untouched.

* fix(hub): never capture the transcript into event/reply snapshots

Replaces the publish-boundary strip with the real fix: don't build
message-bearing snapshots in the first place. emitSessionSnapshot no longer
re-reads the entire transcript from disk on every status flip, and
readCoreSessionSnapshot no longer reads it for any event or reply — a
snapshot is a state notification (status, usage, model, workspace,
checkpoint); the transcript is fetched via the session.messages command.
Checkpoint-restore snapshots (session-versioning-service) are untouched:
restore replies carry messages in their own dedicated field.

* chore(desktop): release v0.0.19

* chore(sdk): release v0.0.81

* chore(cli): release v3.0.60

* fix(vscode): avoid render crash on malformed api_req payloads in combineApiRequests (#13560)

* fix(vscode): stop pinning DeepSeek model count in catalog smoke test (#13600)

* feat(ui): share agent welcome hero (#13567)

* feat(ui): share agent welcome hero

* test(ui): cover welcome hero pointer states

* refactor(ui): keep welcome hero API minimal

* test(ui): verify welcome hero package assets

* fix(ui): inline welcome hero masks

* fix(tools): preserve a file's own CRLF line endings across apply_patch updates (#13512)

* fix(desktop): keep the window title bar draggable across views (#13572)

* fix(desktop): keep window title bar persistent

* fix(desktop): reserve persistent title bar space

* fix(desktop): polish persistent title bar layout

* Sign Windows CLI binaries with Azure Trusted Signing; surface app-control launch errors (#13021)

* feat(cli): sign Windows binaries with Azure Trusted Signing and surface app-control launch errors

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): use _CLI-suffixed signing profile secret, normalize endpoint, fail loud on partial config

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* Tunnel ProtoBus over the existing Host Bridge (#13218)

* feat(core): tunnel ProtoBus over Host Bridge

* fix(core): harden Host Bridge stream lifecycle

* fix(core): serialize concurrent chunked responses per request

Streaming handlers deliver updates fire-and-forget, so two logical
responses for one request_id can be in flight at once. Chunked payloads
made forwarding non-atomic: each chunk write is an await, so concurrent
forwards could interleave their chunk sequences and the receiver --
which reassembles purely by arrival order -- would splice two payloads
into one. Route all forwards for a request through one promise chain; a
failed write rejects every later forward so a torn payload is never
followed by more chunks.

Rename the lock manager's instanceAddress to instanceOwner: it holds an
opaque per-spawn instance ID on the token path and a listener address
only on the CLI-harness path. Delete the caller-less getInstanceByPort
query that interpreted the owner as an address.

Also: document message_json as a legal wire encoding for small
payloads, close the gRPC client when startup fails, note the
intentional discard of the cancellation confirmation, and add the
proto's trailing newline.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* Build and Authenticode-sign a Windows x64 desktop installer in desktop releases (#13607)

* feat(desktop): build and Authenticode-sign a Windows x64 NSIS installer in desktop releases

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): pin OIDC-adjacent actions to commit SHAs in the Windows signing job

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): pin checkout and upload-artifact to commit SHAs in the Windows signing job

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): show agent-created schedules on the Schedules page (#13613)

* fix(desktop): show agent-created schedules on the Schedules page

Schedule hub commands are scoped to the workspace registered by the
connection, but the desktop app's hub client registers the app launch
directory while agent-created schedules live under each chat's own
workspace folder - so they never appeared on the Schedules page.

Grant token-authenticated hub connections (which can already bind any
workspace at registration) explicit cross-workspace schedule access via
an allWorkspaces payload flag, and have the desktop sidecar request it
for routine schedule commands. Workspace-bound clients (local browser
origins) and default CLI behavior stay scoped.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(core): strip allWorkspaces flag from schedule inputs and pin it in the sidecar payload

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make suggested routine template prompts prescriptive about their final output (#13611)

* Make bug hunter routine template prescriptive about its final report

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make remaining routine templates prescriptive about their final output

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop: surface scheduled-task final output — auto-expand submit_and_exit and render its summary as markdown (#13612)

* desktop: auto-expand submit_and_exit and render its summary as markdown

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: render submit summary in full foreground color

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: label the submit row 'Scheduled task completed'

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: label errored submit_and_exit rows as failed

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add tooltips explaining Live and After recording badges on voice input models (#13610)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Remove box shadow from chat message actions row (#13630)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): make the Tauri shell work on Windows (#13632)

- Defer updater installation to the user-initiated restart on Windows:
  install() launches the NSIS installer and exits the process immediately,
  so the background cycle now downloads only and stages the bytes, and
  restart_to_apply_update installs them after stopping the sidecar.
- Spawn child processes (sidecar, git, cmd /C start) with CREATE_NO_WINDOW
  so the GUI-subsystem app doesn't pop visible console windows.
- Fall back to USERPROFILE when HOME is unset resolving the MCP settings
  path, matching the sidecar's homedir().
- Reap the sidecar after the Windows hard-kill so its exe file lock is
  released before the NSIS installer replaces it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop watching agenda spec dirs while the todo tool is disabled (#13629)

* fix(core): stop watching agenda spec dirs while the todo tool is disabled

Since #13530 disabled the agent todo tool, the Agenda UI, and the
automation pump, the hub still created fs.watch watchers on the global
agenda specs dir and on every workspace root recorded in the task store
(at startup and on scope access). Nothing consumes the watcher-driven
task events while the feature is off, and the task.* hub commands
already reconcile spec files on demand, so the watchers are pure
overhead - one OS watch handle per known workspace.

Wire watchFiles to AGENDA_TODO_TOOL_ENABLED the same way
automationEnabled is, preserving a host's explicit watchFiles opt-out
for when the flag is turned back on. Schedules are unaffected: the
schedule list has no file watcher and updates through hub commands and
published schedule events.

* fix(core): reconcile external spec edits inside updateTask

With the spec watchers off there is no background reconciliation, so a
task spec edited directly on disk made every same-store task.update fail
the signature check with "task spec changed outside the manager" until
an unrelated task.get or task.list happened to reconcile the scope.

Reconcile the task's scope at the start of updateTask (mirroring what
refreshAndVerifyTaskIntent already does for approve/run), skipping it
when the file reconciler itself is the caller to avoid recursing from
reconcileFileStore. An external edit now surfaces as the store's normal
stale-revision conflict, and a re-read-and-retry succeeds. This also
closes the pre-existing watcher debounce race for updates.

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently (#13565)

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently

Port the cline-provider refresh semantics to openai-codex and oca:
a transient refresh failure (network error, timeout, server 5xx) with an
already-expired access token now rethrows instead of returning null.
A null return means the refresh token was REJECTED and re-auth is
required; treating an outage blip as a rejection is what turned it into
a forced 'openai-codex requires re-authentication.' task stop while the
settings UI still showed the user as signed in.

Both providers also emit user.auth_refresh_soft_failure telemetry on
transient failures (the 'prevented logout' counter the cline provider
already has) and attach status/errorCode details to the genuine
invalid_grant logout event.

* refactor: collapse duplicate soft-failure telemetry branches and test

Review feedback: compute tokenExpired once and emit the soft-failure
event once in both providers, then return current credentials or
rethrow. Fold the codex soft-failure telemetry assertions into the
existing still-usable-token test instead of a near-duplicate case.

* fix: make OpenAI Codex (ChatGPT subscription) sign-in fail loudly instead of silently dead-ending (#13537)

* fix: make OpenAI Codex sign-in fail loudly instead of silently dead-ending

When callback port 1455 is already in use (e.g. by the Codex CLI or a
previous pending sign-in), startLocalOAuthServer returns a no-op server
and loginOpenAICodex would open the browser anyway, then dead-end:
the callback could never be received, and in the VS Code extension the
user just saw nothing happen after clicking 'Sign in to OpenAI Codex'.

- loginOpenAICodex now fails fast with an actionable 'port in use'
  error before opening the browser, unless the host provides manual
  code entry (the CLI's paste fallback keeps working)
- surface OAuth redirect errors (e.g. access_denied) instead of
  collapsing them into 'Missing authorization code'
- the extension dedupes concurrent sign-in clicks: a re-click re-opens
  the auth page of the pending flow instead of spawning a second flow
  that would collide with our own callback server
- browser-open failures now show an error message with the URL to
  open manually instead of only logging
- abandoned-flow timeouts no longer surface a confusing 'Missing
  authorization code' toast

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop host-side codex login dedupe, keep flow identical to CLI

The SDK owns the failure handling now (fail-fast on an unbindable
callback port), so the extension keeps the exact same simple
loginOpenAICodex call the CLI uses. A second click while a flow is
pending gets the SDK's clear port-in-use error, same as running
'cline auth openai-codex' twice would. Keep only the CLI-parallel
onOpenUrlError surfacing (the CLI prints 'open the URL above
manually'; the extension's equivalent is an error toast with the
URL).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(e2e): cover Codex sign-in callback-port failure and redirect errors

Two driven-VS Code tests for the OpenAI Codex (ChatGPT subscription)
sign-in flow:

- with port 1455 occupied on both loopback families, clicking the
  sign-in button surfaces the fail-fast port-in-use toast
- with the port free, the callback server binds and an OAuth redirect
  error (access_denied) propagates to a visible error toast

The second test opens a real browser tab to the OpenAI auth page as a
side effect of the genuine sign-in click.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* feat(core): anchor agent-created schedules in the user's .cline schedules home (#13634)

* feat(core): anchor agent-created schedules in the user's .cline schedules home

Agent-created schedules inherited whichever workspace folder the chat
session happened to run in, scattering user-level routines across chat
and project folders. They were invisible to workspace-scoped listings
elsewhere, tied to folders that may be cleaned up, and each chat's
tasks tool saw a different set when checking for duplicates.

Anchor them in ~/.cline/schedules instead: the hub's scheduled-task
session defaults now resolve to that home (created on demand), so
agent-created schedules live and run in one stable user-level scope.
The tasks tool guidance now tells agents that scheduled sessions run in
the schedules home, so prompts must carry absolute paths to any project
they operate on.

Schedules created explicitly with a workspace (CLI --workspace, desktop
routine wizard) are unchanged, and existing rows keep their current
workspaceRoot - they stay visible through the all-workspaces listing
paths (#13613, #13633).

* test(core): restore any pre-existing CLINE_DIR after the agenda hub test

The test's cleanup deleted CLINE_DIR outright, so an environment that
had it configured would leave later tests in the same worker on the
default storage directory. Save the previous value and restore it.

* test(core): restore CLINE_DIR even when hub test setup throws early

Restoring the override in the try/finally missed failures thrown during
transport construction or start(), before the try was entered. Register
the restore with onTestFinished instead, which runs regardless of where
the test fails.

* fix(desktop): don't show providers as configured without real credentials (#13608)

* fix(desktop): don't show providers as configured without real credentials

The desktop settings marked any provider with a persisted settings entry
as Configured, but legacy VS Code migration and empty saves can seed
entries (e.g. qwen-code, sapaicore) holding only a default model and no
credentials. Move the CLI's isProviderSettingsUsable readiness check into
@cline/core, expose it as a computed 'configured' flag on the provider
catalog, and use it in the desktop's isProviderConnected.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): resync catalog after saves so Configured badge updates live

Optimistic provider mutations can't know the sidecar-computed 'configured'
flag, so after connecting a keyless provider or saving cloud credentials
(e.g. a Vertex project id) the row stayed 'Not configured' until remount.
Silently refetch the catalog after each successful save, guarded by the
existing generation counter so newer edits discard stale responses.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): claim a generation in post-save resync so overlapping refreshes can't apply stale snapshots

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): bump catalog generation on OAuth login success

Every other optimistic provider mutation claims a new generation; the
OAuth success path didn't, so a catalog load or resync still in flight
could arrive late and overwrite the just-connected state.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): resync catalog after OAuth login instead of bare generation bump

The resync claims a new generation (discarding any stale in-flight
response) and its own fetch covers both the new OAuth connection and any
provider saved moments earlier, matching the post-save path.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint (#13626)

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint

Restoring a checkpoint runs git reset --hard, which moves the current
branch pointer. If commits were made after the checkpoint (by the user
or by the agent), the reset silently knocked them off the branch,
leaving them reachable only through the reflog.

Guard the reset: if HEAD no longer matches the commit the checkpoint
was created on, throw a descriptive error (including how many commits
would be dropped) instead of destroying history. Chat-only restore is
unaffected, and users who really want to discard the commits can reset
the branch manually first.

Fixes #13550

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): close the guard-to-reset race with an atomic ref update

The moved-HEAD guard read HEAD, ran further git commands, then reset
unconditionally, so a commit landing in that window could still be
knocked off the branch. Replace the reset's branch move with git's
native compare-and-swap (git update-ref HEAD <new> <old>), which fails
if HEAD no longer points at the verified commit, and follow with a bare
reset --hard to sync the index and worktree to the already-moved HEAD.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: hide history cost estimates for subscription-billed tasks (#13562)

* fix(vscode): hide history cost estimates for subscription-billed tasks

The task-header fix for subscription providers cannot reach history:
history rows render the stored totalCost (an API-rate estimate) and do
not know which provider ran the task, so the history page printed
$X.XXXX on every row and the recent-task chips in an empty chat view
rendered a $ chip even for subscription-billed tasks.

The SDK session records already persist the provider — the CLI's
history view uses it for exactly this — but the VS Code mappers dropped
it. Map it through both transports (HistoryItem.apiProvider for the
state-pushed taskHistory, TaskItem.api_provider for getTaskHistory) and
suppress the dollar figure per row when that provider's
usageCostDisplay is not "show", via a new useUsageCostVisibility
predicate shared by both surfaces.

Rows without a recorded provider (tasks predating the field, legacy
imports) keep showing the stored value — there is nothing to key
suppression on.

* test(vscode): e2e-verify history cost suppression in real VS Code

Seeds SDK session records (one openai-codex subscription task, one
anthropic usage-billed task) into the isolated CLINE_DIR before the
webview loads, then asserts in a real VS Code instance that both the
recent-task chips and the full history page render the dollar figure
only for the usage-billed task. Covers the two boundaries the unit
tests stub: on-disk records reaching getTaskHistory with provider
populated, and the provider listings delivering the subscription mark
to the webview.

* Fix scheduled tasks disappearing after desktop app updates (#13627)

* Fix hub-managed schedules being wiped by cron reconciliation on hub restart

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Require the virtual hub/schedules path when exempting specs from removal reconciliation

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Treat recorded source mtime as proof a spec is file-backed, closing the hub/schedules spoof gap

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(shared): discover global rules at ~/Cline/Rules (#13614)

The VS Code Rules tab resolves the Documents folder via
'xdg-user-dir DOCUMENTS', which prints bare $HOME when no user-dirs
config exists (WSL/headless installs), so it reads and writes global
rules at ~/Cline/Rules. The SDK's rule search paths only covered
~/Documents/Cline/Rules, so those rules never reached the system prompt.
Add the missing path to the search list.

Fixes #13542

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat: add searchable session history (#13420)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* Fix CLI crash when a remote MCP server is offline but enabled (#13639)

Remote (SSE/streamable HTTP) MCP connects run on the session.create
critical path, which the hub caps at 30s. Without a connect budget an
unreachable server spent the full 60s default request timeout (with the
SSE transport stuck in a reconnect loop), stalling session.create past
the hub deadline and tearing the whole session down - the interactive
TUI exited and one-shot runs failed. Stdio servers already have a
bounded initialize budget for exactly this reason; give URL clients the
same treatment with a 10s default connect budget that an explicit
timeout overrides in either direction.

Fixes #13597

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(vscode): prevent E2E worker teardown hangs (#13644)

* test(vscode): capture external URLs in E2E runs

* docs(test): clarify browser capture rationale

* Add a GitHub integration step to the onboarding (#13225)

* Add feature flags to the app

* React to account updates

* Address comments

* Add a GitHub integration step to the onboarding

* validate domain and fix errors on auth

* Hide the step behind a feature flag

* update version

---------

Co-authored-by: John Choi <john.choi@cline.bot>

* fix(ci): stop e2e worker teardown timeouts and deflake hub daemon e2e on Windows (#13646)

* fix(e2e): stop VS Code e2e worker teardown from timing out

The ext-vscode-test-e2e job has been failing on main with 'Worker teardown
timeout of 60000ms exceeded' even though every test passes. Playwright only
reports an Electron app as closed once the process exits AND every holder of
its stdio pipes is gone (ChildProcess 'close' waits on the extra fd3/fd4
pipes Playwright creates for Electron). Any VS Code descendant that outlives
the main process (chrome_crashpad_handler, GLib's 'dconf watch' helper,
xdg-open browser handlers, VS Code 1.135's agent host CLI subprocess that
logs 'unable to kill the process') keeps those pipes open, so app.close()
never resolves and the worker teardown hangs on it until its 60s timeout
fails the job.

Harness fixes, each removing one source of that wedge:

- closeAppForTeardown now SIGKILLs the whole process group (taskkill /T on
  Windows) when app.close() times out, instead of only the main pid — and
  does so even when the main process already exited, which is exactly the
  wedged state. Playwright launches Electron detached, so pid == pgid.
- Launch VS Code with --disable-crash-reporter so no crashpad handler
  outlives the app holding the harness pipes.
- Seed the fresh user-data-dir with chat.disableAIFeatures: true so VS
  Code's own AI features (rolled out via server-side experiments, so CI
  breaks without any repo change) never start their agent host process.
- Drop the page.close() teardown: closing VS Code's last window quits the
  whole app, and ElectronApplication.close() on an already-exited app
  deadlocks; the app fixture's app.close() closes windows itself while the
  app is alive.
- Codex sign-in no longer opens a real external browser under E2E_TEST; the
  codex-oauth test drives the OAuth callback itself, and the browser was an
  orphaned process holding the harness pipes on the runner.

* fix(core): deflake hub daemon e2e tests on Windows runners

sdk-test on windows-latest fails intermittently in the hub daemon e2e
files:

- shutdown.e2e.test.ts dies with a bare 'Error: socket hang up'. That
  message is the ws handshake (http.ClientRequest) failing, not the
  /shutdown fetch (an undici failure prints 'TypeError: fetch failed'):
  a freshly spawned bun daemon on a loaded 2-core Windows runner
  occasionally drops its first accepted connection before writing the
  upgrade response. Real hub clients reconnect with backoff, and the test
  asserts shutdown behavior rather than first-connection reliability, so
  openAuthenticatedSocket now retries transient handshake failures within
  a 15s budget.
- singleton.e2e.test.ts times out waiting for daemon discovery: it still
  used the 10s hang guard that 0cfc90158 already raised to 30s in
  shutdown.e2e.test.ts for the same reason. Use the same 30s guard.
- Raise the e2e testTimeout to 60s so a test that legitimately spawns two
  daemons back to back can survive slow-runner startups instead of the
  discovery hang guard being cut off by the test timeout.

* feat(desktop): render tool output images as attachments (#13643)

* fix(desktop): render tool output images as attachments

Add support for displaying media returned by tool calls (e.g. screenshots)
as rendered images with expand-to-fullscreen capability instead of raw
base64 text. Introduces an `ImageCarousel` component for navigating
multiple images, propagates the expand handler to tool message blocks,
and extracts/validates output media in tool summaries.

* test: cover multi-image and canonical media extraction in tool output (#13645)

extractOutputMedia and the desktop tool-message rendering path were only
ever exercised with exactly one distinct valid image, and
canonicalInlineMedia (MCP-style type: "media" blocks for audio/video/file)
had zero coverage. Add tests for: multiple distinct images in one tool
result (parser + desktop carousel navigation), inline audio via the
mime_type key spelling, canonical video/file media blocks, and rejection
of an invalid canonical image block.

---------

Co-authored-by: Harrison <harrison@cline.bot>

* chore(desktop): release v0.0.20

* feat(sdk): add discovery boundary ahead of Agent Plugins support (#13017)

* ENG-2490: Propagate session aborts to teammates (#13647)

* fix(core): propagate session abort to teammates

* fix(core): persist aborted teammate tasks as cancelled

* fix(core): settle teammate work on session abort

* fix(core): isolate replacement runs from stale aborts

* refactor(core): narrow teammate task status metadata

---------

Co-authored-by: abeatrix <beatrix@cline.bot>

* fix(llms): use AI SDK 7 Langfuse telemetry (#13651)

* fix(llms): use AI SDK 7 Langfuse telemetry

* test(llms): cover Langfuse runtime context

* chore(llms): built-in model list update 1787907289186 (#13663)

* chore(llms): built-in model list update 1787907289186

Result of `bun run build:models`.
Includes updated model list and fixed formatting issues across codebase.

* test(llms): update GLM reasoning toggle expectation

* test: cover session search fallback on hub timeout and rejection (#13642)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

* test: cover sidecar search fallback on hub timeout and rejection

The existing search_sessions tests only exercised the index-hit and
empty-index-fallback paths with an immediately-resolved hub reply.
Add coverage for the two other realistic Hub-connection failure
modes the fallback is meant to tolerate: the hub call rejecting, and
the hub call hanging past the 750ms withSearchDeadline race.

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* fix(core): refresh Cline models from live catalog (#13670)

* feat(ui): share attachment drop zone (#13672)

* feat(ui): share attachment drop zone

* fix(ui): cancel disabled attachment drops

* chore(ui): simplify drop zone surface

* chore(ui): release v0.2.0-next.8

* Chore/bump undici mermaid (#13675)

* chore(deps): bump mermaid to 11.16.1 and raise undici floor to 7.29.0

* chore(deps): patch js-yaml and body-parser in the npm-managed subprojects

* fix(llms): make Langfuse tracer detection survive minified release builds (#13680)

* fix(llms): recognize direct tracer providers

* fix(llms): make Langfuse tracer detection survive minified release builds

Release binaries are compiled with minify enabled, which renames classes,
so initializeLangfuseTelemetry's constructor-name guard never matched
"ProxyTracerProvider" and silently returned readiness=false in every
production build (hub log: "creating span processor" followed by
"initialized readiness=false" with no branch message in between). Dev runs
execute unminified source, which is why the same env vars worked there.

Replace every constructor-name comparison with checks that survive
minification: detect the proxy structurally via getDelegate, distinguish a
recording provider from the no-op fallback by its lifecycle methods, and
confirm our NodeTracerProvider registration by object identity. When a
foreign provider already owns the global slot, attach the Langfuse span
processor to it when it accepts processors, and otherwise shut down the
orphaned provider and report the rejection instead of bailing silently.

Verified by bundling the module with Bun minify:true against the real
OpenTelemetry packages: the previous code reproduces readiness=false
(provider class name mangles to "H2"), the new code initializes with
readiness=true.

* fix(vscode): prevent hook spawn failures from crashing the core process (#13422)

* fix(vscode): prevent hook spawn failures from crashing the core process

A hook child-process spawn failure emitted "error" on HookProcess with no
listener registered, which Node's EventEmitter turns into an uncaught
exception - killing the entire cline-core process instead of failing the
one hook open. Guard the emit behind listenerCount so the rejection (which
StdioHookRunner handles) is the only propagation path.

The trigger was a workspace root that no longer exists on disk passed as
the spawn cwd: Node reports a nonexistent cwd as a misleading ENOENT on
the launcher binary ("spawn /bin/sh ENOENT"). Validate cwd existence in
HookProcess right before spawning - falling back to no explicit cwd with
a warning that names the missing directory - and when a spawn still fails
ENOENT because the directory vanished in between, name it in the error
message instead of blaming the shell.

* fix(vscode): fail hooks with a missing working directory instead of relocating them

Running a hook whose assigned cwd no longer exists from the host
process's own working directory would let its relative paths read and
write an unrelated location (e.g. the IDE install directory). Reject
before spawning, with an error naming the missing directory; the runner
reports the hook as failed and the task continues. Also carry pre-spawn
failure messages into HookExecutionError details so the cause is not
reduced to a bare "exited with code 1".

* fix(vscode): thread task id into hook runner creation so execution telemetry fires (#13547)

The SDK hooks adapter created every hook runner without a task id, and
StdioHookRunner gates all captureHookExecution calls on one being set —
so the next variant emitted zero hooks.execution events while discovery
telemetry fired normally. Pass the task id (and tool name for the tool
hooks) at all five factory.create call sites, and pin the threading
with a regression test.

* chore(desktop): cut 0.0.21-beta.1

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: JasmineLCY <38378321+JasmineLCY@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Haley Park <haleypark.design@gmail.com>
Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>
Co-authored-by: yzxcj797 <54314860+yzxcj797@users.noreply.github.com>
Co-authored-by: yzxcj797 <yzxcj797@users.noreply.github.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: 𝓜𝓲𝓼𝓼𝓪𝓻𝓲 𝓐𝓱𝓲𝓵 🌿 <143264692+missarii@users.noreply.github.com>
Co-authored-by: Dominic Cooney <dominic.cooney@cline.bot>
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Harrison <harrison@cline.bot>
Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: TheRealSpencer <32678829+TheRealSpencer@users.noreply.github.com>
2026-08-31 12:14:01 -07:00
+14 dd4d80b7b3 chore(desktop): sync latest main into desktop experimental (#13648)
* fix(vscode): continue the surviving session on resume instead of rebuilding with the original task text (#13175)

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilt the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

The preserved conversation history is the source of truth on resume, so
the fallback prompt now just asks the model to reassess the history and
continue, matching the legacy resume prompt which also never resent the
original task. User-typed text still takes precedence when provided.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): continue the surviving idle session on follow-ups instead of rebuilding

Stopping a turn keeps the session alive, but every idle follow-up (bare
Resume after Stop, and typed follow-ups after a completed turn) tore
that session down and rebuilt it from persisted task history before
sending. Continue the matching idle session in place instead, the same
way the CLI reuses the live session after an abort. Rebuilding from
history now only happens when no live session matches the displayed
task (task opened from history, extension host reload).

A bare resume still needs a prompt to start a turn, so it sends the
neutral [TASK RESUMPTION] prompt (shared with the rebuild fallback and
hidden from the transcript); user-typed content is echoed and sent
as-is. If the send lands while the abort is still settling, the runtime
auto-queues it and drains once the abort completes.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): consolidate follow-up send paths in SdkFollowupCoordinator

Now that idle follow-ups continue the live session in place, the
two-mode sendToActiveSession helper was redundant: its non-queued branch
duplicated continueIdleSession minus the bare-resume prompt. Split it
into a single-purpose queueToActiveSession and fold the idle no-task
send into continueIdleSession, flattening askResponse's decision tree
to: queue onto a running turn, continue a matching live idle session,
rebuild from history, or abandon.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): reuse the existing neutral resumption prompt for bare resumes

Drop the newly invented long resumption wording in favor of the phrase
that already existed as the no-history fallback and that the transcript
hiding logic and test fixtures recognize: '[TASK RESUMPTION] Please
continue where you left off.' The net change to resumeSessionFromTask
against main is now just deleting the branch that resubmitted
historyItem.task as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilds the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

Bare resumes now always use the neutral prompt that already existed as
the no-history fallback; user-typed text still takes precedence. This
matches the legacy resume prompt (responses.taskResumption), which only
ever included user-supplied text as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): hide synthetic prompts from the queued-prompt echo

A send that races a settling abort is auto-queued by the runtime, so a
bare Resume can reach the pending_prompt_submitted echo carrying the
synthetic [TASK RESUMPTION] prompt. Echoing it leaked model-facing text
as a visible user bubble and shifted the visible-user-message ordinals
that edit/regenerate mapping relies on. Filter synthetic prompts with
isSyntheticUserPrompt, keeping user attachments visible (matching
isSyntheticSdkUserMessage semantics).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): preserve LiteLLM input token limits (#13293)

* fix(vscode): preserve LiteLLM input token limits

* fix(vscode): prefer live LiteLLM model metadata

* fix(vscode): generalize private catalog metadata

* test(vscode): preserve llms exports in vscode lm mock

* fix(vscode): point provider signup URLs at their API key pages (#13337)

* fix(vscode): point Mistral signup URL at the general API keys console

The Mistral provider's signup link led to the Codestral console, which
issues Codestral-scoped keys that fail with 401 on api.mistral.ai — the
endpoint the provider actually calls. Point it at the general API keys
page instead.

Fixes #13288

* fix(vscode): deep-link DeepSeek and Fireworks signup URLs to their API key pages

Both pointed at marketing homepages; link straight to the key-creation
pages instead, matching the rest of the registry and the desktop app's
provider-key-urls map.

* fix(ci): always build the legacy bundle from the legacy-extension branch (#13349)

The combined-VSIX workflow took legacy-ref as a free-form dispatch input
with no publish-time validation (next-ref has one: publish requires main).
Any typed ref — a PR merge ref, an unprotected branch — would be built
into the published VSIX by the environment-less build job, and the publish
environment approver only ever sees an opaque prebuilt artifact, so the
approval protected the marketplace PAT but not the shipped bytes.

Remove the input entirely and hardcode the protected legacy-extension
branch, which makes that branch's protection rules load-bearing for
releases. The tested-sha pinning between test-legacy and build is
unchanged. publish-extension skill dispatch command updated to match.

* fix(ci): lock the legacy publish workflow to the legacy-extension branch (#13350)

The branch dispatch input was a free-form string with no validation. Both
jobs checked it out and ran full npm lifecycle scripts from it: the publish
job next to VSCE_PAT/OVSX_PAT (and npm run publish:marketplace executes a
script from that same ref with the PATs in env), and the test job with NO
environment approval at all while inheriting the workflow-level
contents/packages/checks/pull-requests write grants. A dispatch pointing at
e.g. refs/pull/N/head would run outside-contributor code with the
marketplace keys behind one approval, or with a repo-write token behind
none.

Remove the input and hardcode the protected legacy-extension branch, drop
the workflow-level permissions to contents: read, and elevate only the
publish job to contents: write (tag push + GitHub release). The branch
input's default was legacy-extension, so normal publishes are unchanged.
publish-extension skill dispatch command updated to match.

* fix(vscode): SDK remote-config parity — refresh coordination, session gating, and fail-closed opt-out (#13226)

* feat(desktop): native notifications (#13166)

* feat(desktop): native notifications

* macos target

* fix(desktop): isolate macOS dev app identity

* fix(desktop): address notification review feedback

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched (#13310)

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched

Toggling an auto-approve setting while a task is open writes
autoApprovalSettings into the StateManager's task-settings overlay
(updateAutoApprovalSettings -> setTaskSettings). The SDK controller never
cleared that overlay on clearTask/showTaskWithId (the legacy controller
did), so after New Task the stale overlay kept shadowing global settings
in getGlobalSettingsKey(): toggle RPCs were accepted into global state,
but every posted state still carried the overlay's old version, which the
webview rejects as not newer - the auto-approve checkboxes froze forever.

Restore legacy parity in SdkTaskControlCoordinator: drop the overlay
(persisting pending writes first) in clearTask() and before installing a
different task's proxy in showTaskWithId().

Fixes #13260

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* changeset

* test(vscode): add end-to-end regression test for auto-approve freeze after New Task

Wires the real StateManager, the real updateAutoApprovalSettings handler,
and the real SdkTaskControlCoordinator.clearTask() together with the
webview's version gate modeled on ExtensionStateContext, pinning the
end-to-end invariant behind #13260: checkbox toggles must keep reaching
the webview after a mid-task toggle followed by New Task. Verified the
test fails when the clearTaskSettings() call is removed from clearTask().

* fix implicit any in regression test

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): show provider web-search support under the settings toggle (#13328)

* feat(desktop): show provider web-search support under the settings toggle

The global Web search toggle silently does nothing unless the session's
provider offers native web search, which made the setting read as if it
worked with any provider. The desktop General settings row now explains
that only providers with built-in web search honor it, and shows a live
status line: which connected providers are ready to use it (no extra
setup needed), or an amber warning with a link to the Models section
when none of them support it.

Support is resolved in the webview via a new providerOffersModelTool
helper in @cline/llms (browser export), sharing the same builtin-manifest
source of truth as the runtime's supportsModelTool attachment check.

* fix(desktop): address review — refetch web-search status on catalog invalidation, clarify per-model support

Greptile P2: the one-time catalog fetch could race an in-flight provider
save and show stale status; the row now refetches when the provider
catalog cache is invalidated (fired after saves complete).

Greptile P1: the ready line implied every model on the provider works;
Vertex excludes Claude routes, so the copy now scopes the promise to
models that support it.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* feat(ui/desktop): collapse finished runs into a work summary and remove hover-state dead space (#13315)

* feat(ui): add WorkActivity collapsed-run summary and float message actions as a pill

WorkActivity/WorkActivityTrigger/WorkActivityContent fold a finished agent
run's working rows (tool calls, thinking traces, narration) behind a single
"Worked for 4m 12s · 14 tool calls" disclosure built on the shared animated
disclosure primitives, with formatWorkActivityLabel/formatWorkDuration
exported for consumers.

Message hover actions no longer rely on the transcript reserving blank space
below each message: the action row is now a self-backed pill (border,
blurred background, shadow) that floats over whatever follows, so
conversations can pack rows tightly without hover chrome colliding with the
next message.

* feat(desktop): collapse finished runs into a work summary and tighten chat spacing

collapseCompletedWork post-processes the grouped transcript: once a run ends
on assistant text with no further tool calls, its working rows fold into one
expandable WorkActivity row while the final answer stays visible. Runs are
delimited by user messages; the trailing run only collapses when the session
has stopped running and actually produced an answer, so live streams and
cancelled/failed tails keep their rows. Assistant messages carrying images
or media are treated as deliverables and never collapse.

The conversation list gap drops from gap-8 to gap-4 now that hover actions
are self-backed pills that need no reserved space, and user messages add
their own top margin so turn boundaries stay visually distinct.

* refactor(ui/desktop): work summary label wording, flat expansion, stable in-run rhythm

Feedback round on #13315:

- Label reads "Worked for 4m 12s and made 14 tool calls" instead of joining
  with a dot; without a duration it falls back to "Made N tool calls".
- Expanded work rows render at transcript level — no rail or extra indent —
  since tool rows and thinking traces already carry their own nesting when
  expanded. The work content keeps the tight working-row rhythm.
- Live working rows (thinking traces + tool calls) now group into a 'run'
  render item with the same tight 0.25rem rhythm, so there is no oversized
  gap under a "Thought for Ns" row and every row keeps its exact position
  when the finished run folds into the work summary. A trailing
  answer-in-progress stays outside the group at transcript level, and pure
  prose spans keep normal spacing.
- The transient "Thinking..." indicator moves inside the transcript column
  and mirrors a trigger row's geometry, so the first real row replaces it in
  place with no jump.

* style(ui/desktop): hover-pill metrics, right-pointing work chevron, scroll and spacing fixes

Another feedback round on #13315:

- Hover action pill: +2px internal padding, a trailing inset after the
  timestamp (it sat flush against the pill border), and more clearance
  between the message content and the pill (2px -> 6px; the hover bridge
  grows to match).
- The work summary chevron points right while collapsed and continues
  counterclockwise to point up when expanded.
- Conversation bottom padding drops pb-20 -> pb-8: the composer sits below
  the scroller, so the padding only needs to clear a pinned action pill.
- Sending a message scrolls back to the bottom even if the reader had
  scrolled up (new AutoScrollOnSend on the user-message count, which ignores
  optimistic-bubble re-keying; @cline/ui now exports useConversation for
  this).
- An assistant answer directly under its run's working rows pulls itself
  0.5rem closer than the full transcript gap.

* style(desktop): leave a visible gap between a pinned action pill and the composer

pb-8 exactly matched the pill's ~40px footprint, so the last row's hover
actions sat flush against the composer top; pb-12 restores ~8px of daylight.

* style(desktop): widen the gap between the pinned action pill and the composer to ~24px

pb-12 left only ~8px of daylight under the pill; pb-16 reads comfortable
without reverting to pb-20's dead space.

* fix(desktop): keep the thinking indicator at the working-row offset mid-run

The indicator matched a trigger row's geometry but sat a full transcript gap
(1rem) below the last working row, while the tool/thinking row replacing it
joins the tight run group at 0.25rem — a visible upward jump. When the last
transcript item is working rows (or streamed assistant output), the
indicator now pulls up to the same tight offset; only at the start of a run,
under the user message, does it keep the normal gap.

* style(ui): calm the hover actions surface per team feedback

Borderless rectangle instead of the bordered pill: radius drops to
var(--radius), the side padding goes entirely (the icon buttons carry their
own hit areas), and the vertical padding halves. Blurred background and
shadow stay so it remains legible over following content.

* feat(ui/desktop): full-band hover reveal and iOS-style disclosure easing

The hover actions only appeared while the pointer was inside the message
box itself. The invisible bridge under each message now spans the full
height of the band the floating actions occupy (full row width), so
hovering anywhere in that strip reveals them. Sibling row types
(.cline-chat-tool, .cline-chat-work, and the desktop's run/tool groups)
become position: relative so they paint above the bridge — their own
content keeps its hover and clicks, and the bridge only wins in the band's
genuinely empty space.

All expandable rows (work summary, tool panels, thinking) open and close on
a 240ms symmetric ease-in-out cubic-bezier instead of the 60ms snap, with
chevron rotation on the same curve. Reduced-motion still disables both.

* revert(ui/desktop): drop the full-band hover reveal; quicken disclosure easing to 180ms

The full-band hover bridge (and the position: relative changes that made it
safe) is reverted per feedback — back to the narrow bridge that only spans
the gap under the message. The iOS-style ease-in-out on disclosures stays
but speeds up from 240ms to 180ms.

* fix(ui): recover live tool diffs that mount as a blank pierre skeleton

Live-streamed edit rows could show an empty diff for the whole run, with the
diff only appearing after the collapsed work row was expanded (fresh mount).
Root cause, confirmed by driving a live session and inspecting the element:
React StrictMode double-invokes @pierre/diffs' ref callback; the first
instance's async highlight work aborts on its immediate cleanup, and the
second instance adopts the abandoned half-rendered shadow tree as if it were
complete prerendered output — zero height, no code, no theme stylesheet,
permanently. A rendered diff always carries style[data-theme-css] in its
shadow root, so ToolFileDiff now checks for it shortly after mount and
remounts FileDiff (bounded attempts) when missing; the fresh host element
takes the normal render path and recovers within ~400ms. Verified live: the
diff now renders during the run.

* fix(desktop): keep interrupted runs expanded even with partial trailing text

The trailing-run collapse gated on 'ended with assistant text', which
misread a Stop that landed mid-answer as a finished run and folded the tool
calls the user wants to inspect. The gate is now the terminal status itself:
only completed (or restored-idle) sessions collapse the trailing run;
cancelled/failed/error tails keep their rows regardless of partial text.
(Greptile P1 on #13315 — matches the PR's stated rule.)

* feat(ui): share the markdown pipeline, chat polish, and ThinkingBlock across products (#13323)

* feat(ui): share the markdown pipeline, chat polish CSS, and ThinkingBlock

The desktop app and the cloud dashboard both consume @cline/ui yet rendered
assistant output differently, because Markdown policy and the thinking-trace
row lived app-side. This moves the shareable parts into the package:

- components/markdown (new export): the lazy Shiki code highlighter (GitHub
  light/dark, pinned language set) and agentMarkdownControls — the standard
  Streamdown configuration. streamdown/shiki/@shikijs/* become optional peer
  dependencies, mirroring @pierre/diffs.
- components/markdown.css: the desktop's chat polish moves in — chat-scale
  headings, outside list markers, single quiet code blocks with a
  hover-revealed copy control, table cards. Kept unlayered so it beats
  Streamdown's layered Tailwind utilities without !important.
- ThinkingBlock + formatThoughtLabel in agent-chat: the standard thinking
  row (brain icon, Thinking/Thought-for-Ns label, streaming shimmer, rail
  presentation, capped scrollable body). The shimmer and the
  reasoning-hover-suppression rule move into agent-chat.css; triggers gain
  the color transition the desktop applied locally.

Version bumps to 0.2.0-next.5 for the dashboard to pick up.

* refactor(desktop): consume shared markdown and thinking primitives from @cline/ui

The local Shiki highlighter, Streamdown controls, chat markdown polish CSS,
streaming-title shimmer, and reasoning hover-suppression rule are deleted in
favor of the @cline/ui versions (the highlighter test moves to the package's
suite). ReasoningBlock becomes a thin wrapper that hands MemoizedMarkdown to
the shared ThinkingBlock, and formatThoughtLabel re-exports from the package
so grouping code and tests keep their import path.

globals.css now imports @cline/ui/components/markdown.css (unlayered, so the
polish keeps beating Streamdown's layered utilities); the app keeps only what
is genuinely app-specific: link/image policy in markdown.tsx, selectability
rules, accent palettes, and the view-enter transition.

* style(ui/desktop): make thinking-trace prose legible

Thinking body text rendered too faint: plain muted-foreground plus the
desktop's font-thin weight. The shared thinking content now leans 75% of the
way back toward the body text color (still slightly de-emphasized), and the
desktop drops the thin font weight.

* ci(ui-publish): build @cline/shared before ui typecheck (#13354)

@cline/ui's generated-media imports @cline/shared/browser, which resolves to
shared's dist output. The build-shared step sat after typecheck/test/build,
so the first ui-publish dispatch since #13025 failed at Typecheck UI with
TS2307. Move the step to right after install.

* fix: run_commands object form without args routes through the shell instead of failing with ENOENT (#13336)

* fix: run_commands object form without args routes through the shell

The structured { command, args? } form of run_commands was always spawned
directly with shell: false. When a model emitted a full command line in
command with no args (e.g. { command: "echo hello" }), spawn failed with
ENOENT for any command containing a space, breaking command execution for
the whole session.

Direct exec now only applies when a non-empty args list is provided; the
object form without args is routed through getShellInvocation like the
string form. Schema descriptions are tightened so models put arguments in
args instead of embedding them in command.

Fixes #13279

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: trim structured-command schema descriptions

The union schema is only used for lenient validation of input the model
already sent; its descriptions never reach a model prompt. Keep them
short instead of restating executor behavior.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: simplify direct-exec comment in shell executor

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert: keep original structured-command schema description

The description never reaches a model prompt and the executor now handles
both shapes, so the wording change was cosmetic noise.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: gate direct exec on args key presence, not array length

Review feedback: an explicit empty args array is intentionally structured
input and stays direct exec; only an object with no args key is treated
as a full shell command line. Matches the key-presence rule already used
by the VS Code host's formatCommandForTerminal. Also replaces the
empty-args shell test (which was PowerShell-incompatible) with a test
pinning the direct-exec contract.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: normalize Gemini custom base URLs for legacy host-root values (#13329)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs: add GLM-5.3 to ClinePass models and reference pricing (#13357)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): stream run command output (#13179)

* feat(desktop): stream run command output

* fix(sdk): clean up detached command logs

* fix(sdk): reap detached logs after hub restarts

* fix(sdk): preserve live detached command logs

* fix(desktop): harden live command progress

* fix(sdk): recover detached logs for local hosts

* fix(desktop): reconcile command output tool rows

* fix(sdk): retain logs for surviving commands

* fix(core): prevent PID reuse from retaining detached logs

* fix(core): preserve detached logs on probe failures

* fix(core): retain detached logs during probe outages

* fix(desktop): resolve leftover merge conflict in messages projection test

Combine both sides of the assertion: main's incremented per-block
createdAt projection and this branch's toolCallId/hookEventName meta.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): make TUI dialog colors follow theme changes live (#13355)

* fix(cli): make TUI dialog colors follow theme changes live

Dialog content previously read the static palette constant, so open
dialogs (including the theme picker itself) kept the default dark-blue
accents while scrolling through theme previews. Add getDialogPalette /
useDialogPalette, which resolve dialog colors from the active theme's
dialog accents and re-render on every theme change, and migrate all
dialog-rendered components to it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(cli): derive dialog panel background from the active theme

Dark themes now lift their own background one OKLAB step for the dialog
surface, so panels keep the theme's hue instead of the library's fixed
#262626. DialogThemeSync pushes the surface into the dialog container
for new dialogs and repaints open panels, so the surface also follows
live theme previews. Light themes keep the neutral dark panel to match
the dark accent fallback and the light-on-dark dialog text.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: skill slash commands load via the skills tool instead of expanding into the user message (#13327)

* fix(desktop): show typed slash command instead of expanded skill markdown

The sidecar expands /skill and /workflow tokens into their instructions
before dispatching, so the runtime's persisted transcript only contains the
expanded text. After a turn (and when reopening a session) the webview
re-hydrates from that history and rendered the whole SKILL.md body as the
user's message; queue events echoing the expanded prompt could also add a
second user bubble, and fresh sessions were titled with the markdown's first
line. The CLI never shows this because its TUI keeps the typed text in its
own transcript and only sends the expanded prompt to the model.

Mirror that separation inside the desktop sidecar's display boundaries:

- history projection (readSessionMessages) inverts user text that starts
  with a configured command's instructions back to '/name remainder',
  which also repairs sessions recorded before this fix
- queue snapshots and chat_queued_prompt_start events echo the typed
  prompt recorded at expansion time, so the webview's optimistic-bubble
  re-key matches again
- an untitled session sent an expanded prompt gets titled from the typed
  command instead of the instructions' first line

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't overwrite a mid-turn rename with the typed-command title

The untitled check ran before dispatch, so renaming a fresh slash-command
session while its first turn was running got clobbered by the post-turn
typed-command title. Re-check at write time and only replace a missing title
or the one the runtime auto-derived from the expanded prompt.

Also documents the inherent prefix-inversion ambiguity flagged in review:
text hand-typed with a command's exact instructions persists byte-identically
to that command's expansion, so stored history alone cannot distinguish them.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop expanding skill commands; let the skills tool load them

Pasting the skill body into the prompt is why the transcript could ever show
it: the desktop webview re-hydrates from the runtime's persisted history, so
whatever the sidecar splices into the user message renders as if the user
typed it. The runtime already registers the skills tool, whose description
requires the model to invoke it whenever the user references a slash command
— so send the typed /skill text through and let the tool deliver the
instructions as a tool result (previously they arrived twice: pasted and via
the tool). The persisted user message, session title, and queue entries are
then simply the typed command, which deletes the typed-prompt registry, the
queue event/snapshot rewriting, and the title machinery from the previous
approach.

Workflows are not served by the skills tool and keep textual expansion, so
the read-time display inverter stays: it collapses expanded workflow prompts
— and skill prompts persisted before this change — back to the typed
/command in the history projection.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): option to keep skill slash commands typed for the skills tool

resolveRuntimeSlashCommandFromWatcher (and the hub snapshot proxy) accept
expandSkillCommands: hosts whose sessions register the skills tool pass
false so the typed /skill goes through and the model loads the instructions
as a tool result, keeping the persisted transcript as what the user typed.
Workflows always expand — the tool does not serve them. isSkillsToolAvailable
exposes the catalog check hosts use to decide (yolo preset and the skills
tool toggle leave textual expansion as the only delivery path).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): skill slash commands load via the skills tool instead of expanding

The TUI user-command wrap and buildUserInputMessage now keep a typed /skill
as-is when the session's mode/toggles register the skills tool, matching the
desktop app; workflows keep expanding, and yolo (zen) keeps expanding skills
because its preset has no skills tool. This also fixes CLI resume/history
surfaces showing the skill body: the persisted user message is now the typed
command.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): keep configured skill slash commands typed for the skills tool

expandSlashCommands no longer splices a configured skill's instructions into
the model text; the SDK session's skills tool delivers them as a tool result
(previously they arrived twice). Builtin pseudo-skills like /deep-planning
are not served by that tool and keep expanding, as do workflows.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): use the shared skill-expansion option in the sidecar

Replaces the sidecar's workflow-detection dance with core's
expandSkillCommands option and gates on isSkillsToolAvailable, restoring
textual expansion where the tool is missing (yolo mode or the skills tool
toggle) — a gap in the previous desktop-only change.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): drop the display inverter for expanded transcripts

Accepted trade-off to keep the change minimal: sessions recorded before
skills switched to the skills tool, workflow sends (deprecated), and
yolo-mode skill sends persist expanded instructions and now render that text
as-is instead of being collapsed back to the typed /command at projection
time.

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>

* Use fixed selection chevron in account dialog to match other dialogs (#13364)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): align system prompt with session mode (#13361)

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): finalize queued turns on chat_done with canonical history reconcile (#13330)

Turns that settle through the event stream (queued prompts, including the
first prompt of a fresh session) resolve their send() RPC early, so nothing
cleared the streaming shimmer or reconciled live-streamed content against
the persisted transcript at turn end. A turn whose deltas were incomplete
stayed visually streaming forever and only healed when a later non-queued
send rehydrated history.

chat_done (and chat_session_ended / the queue-drain double check) now clears
the active assistant streaming id and schedules a short-delayed
read_session_messages + applyCanonicalHistory, guarded by turn epoch,
session id, and in-flight send submissions so it never clobbers a newer
turn or duplicates the blocking send path's own finalization.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.14

* fix(clients): filter non-chat models from chat pickers (#13317)

* fix(clients): filter non-chat models from chat pickers

* fix(clients): align chat model eligibility

* fix(desktop): strip user_input envelope when copying a user message (#13369)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>

* docs: show DeepSeek V4 peak and off-peak pricing (#13312)

* docs: update DeepSeek V4 average pricing

* docs: show DeepSeek peak and off-peak pricing

* docs: add GLM-5.3 reference pricing (same as GLM-5.2)

* docs: add GLM-5.3 to ClinePass models table

* fix(llms): display billed gateway cost (#13385)

* fix(shared): run PowerShell commands with fail-fast error semantics (#13358)

* fix(shared): run PowerShell commands with fail-fast error semantics

The run_commands PowerShell wrapper never set $ErrorActionPreference, so
the default 'Continue' applied: a pipeline erroring per item (e.g. a
malformed Where-Object over Get-ChildItem -Recurse) emitted one error
record per enumerated file - tens of thousands of stderr records on
large trees, looking like a hang - and could still resolve as SUCCESS
with exit 0.

Prepend $ErrorActionPreference='Stop'; to the script content executed
by the ScriptBlock so the first error terminates the command with a
non-zero exit and a single error message. Concatenated on the same line
as the user command so error line numbers stay unshifted.

Fixes #13285

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(shared): set the fail-fast preference in the bootstrap scope

Setting $ErrorActionPreference='Stop' by string-prepending it into the
scriptblock source displaced a leading param(...) from its mandatory
first-statement position, so scripts beginning with a param block failed
with CommandNotFoundException. Preference variables are dynamically
scoped, so setting Stop in the -Command bootstrap gives the invoked
scriptblock identical fail-fast semantics while keeping the user script
byte-identical (param works, error positions unshifted) and drops the
doubled-quote escaping.

* docs(shared): document the fail-fast tradeoffs in the PowerShell wrapper

Stop promotes every non-terminating error, not only per-item pipeline
floods: partial-result commands (recursive listings over access-denied
junctions) now stop at their first error, and Windows PowerShell 5.1
turns in-script stderr redirection of succeeding native commands fatal.
State this in the wrapper comment as a deliberate tradeoff, with the
GitHub Actions precedent and the per-command opt-outs.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* ci: stop over-long changelogs from silently dropping release Slack posts (#12955)

Slack section blocks reject text longer than 3000 characters. The Slack
action logs that rejection as ##[error] but does not fail the step, so an
over-long changelog drops the release announcement while the run stays
green — cline@3.0.50 (3272 chars) published to npm, tagged, and cut a
GitHub release with no Slack post and nothing red to notice.

Every publish workflow pasted the changelog section verbatim into one
section block, so all six were exposed; the SDK, desktop, and extension
sections were only 150-350 chars under the ceiling.

Add a slack_content output alongside content: unchanged when the section
fits, otherwise trimmed on a line boundary with a link to the full
release notes. Only the Slack payload uses it — GitHub release bodies and
the desktop updater manifest still get the whole section.

* ci: tidy workflow cache config and job permissions (#13403)

Publish workflows now always do clean npm installs (no dependency
cache in their test gates), the e2e workflow's cache keys are
exact-match only, and the e2e job drops an id-token permission it
never used.

* Rename desktop app from "Cline Code" to "Cline" (#13401)

* Rename desktop app from Cline Code to Cline

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Format touched Rust test assertions

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): surface provider-executed tool activity as observational events (#13300)

* fix(llms): surface provider-executed tool activity as observational events

Provider-executed tool parts (e.g. every tool the Claude Code CLI runs
inside its own session) were dropped by the model-tool guard added for
web search: only declared model tools were re-emitted, everything else
hit continue with nothing yielded. Those sessions modified the workspace
with no tool activity in runtime events, transcripts, or the UI.

Route all providerExecuted parts onto the observational path instead:
emit execution-tagged tool-call-delta and tool-result events, matched by
tool-call ID for providers that omit the flag on the result half. They
stay out of AgentRuntime's execution/approval loop, and the runtime
already persists them as modelToolActivities and projects them for
display.

The AgentModelEvent tool-result variant widens toolName from
ModelToolName to string to carry the provider's own tool names.

* fix(agents): keep turns that are only provider-executed tool activity

A turn consisting solely of observational tool activity has an empty
assistant content array - the activity lives in message metadata, since
projecting it into content would replay tool_use blocks the model never
gets results for. The empty-content guard threw on such turns, erroring
the run and losing the activity from the transcript. Count model-tool
activity as content for the emptiness check (error finishes still
throw); replay stays safe through the codec's empty-content placeholder.
Also drop the trailing text delta from one gateway test so the tool-only
stream shape stays covered end to end.

* feat: allow agents to create scheduled tasks (#13331)

* feat(core, desktop): add durable todo agenda

* fix(desktop): secure todo approvals and track tool usage

* fix(desktop): clean up failed approval delivery

* fix(desktop): authenticate approval connections

* fix(desktop): cancel approvals on broadcast failure

* fix(desktop): authenticate development approvals

* fix(desktop): harden development approvals

* test(core): make task paths cross-platform

* fix(desktop): serialize approval readiness

* refactor(core): unify todo and schedule tools

* feat(core): distinguish user todos from agent suggestions

* fix(core): hide tasks tool in yolo mode

* fix(core): enforce schedule workspace scope

* fix(core): bind schedule scope to hub connection

* fix(core): establish task scope at hub startup

* fix(core): scope task automation by workspace

* test(core): normalize workspace path expectations

* test(core): serialize Windows CI workers

* fix(core): reject unregistered schedule authority

* fix(desktop): guard task execution commands

* fix(core): avoid polynomial regex in mention parsing

* fix(core): address schedule tool review feedback

* fix(core): bind websocket clients to hub workspace

* fix(core): flatten tasks tool input schema

* fix(core): authorize multi-workspace hub clients

* test(core): type hub transport authority mock

* fix(cli): register a workspace client for remote schedule commands (#13398)

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): treat ClinePass as OAuth-managed in the chat credential gate (#13404)

* fix(desktop): treat ClinePass as OAuth-managed in chat credential gate

ClinePass shares the Cline account OAuth credentials (its auth handler
stores under the "cline" provider), so the webview never sees a plain
API key for it. The chat pre-flight check only exempted cline/oca/
openai-codex, so switching to ClinePass while signed in via OAuth
blocked with "Missing API key" even though the sidecar resolves the
stored access token fine (which is why the CLI worked).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: format helpers.test.ts with biome

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): stack code block lines when streamdown lineNumbers is off (#13412)

streamdown renders each Shiki token line as a bare inline span with no
newline text between non-empty lines, and only applies its block line
class when lineNumbers is on. With lineNumbers off (the desktop app's
config) every multi-line fenced block collapsed into one run-on line.
Make the direct line spans under code-block-body display: block in the
shared markdown.css; empty lines keep their height via their lone "\n"
child under white-space: pre.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): work summary undercounts wall time when pre-tool thinking attaches to the answer (#13413)

* fix(desktop): anchor work summary duration on the answer row, not attached pre-tool reasoning

The collapsed 'Worked for Xs' row undercounted wall time whenever a turn's
assistant message contained thinking + tool_use with no narration text: the
canonical projection emitted the reasoning-only row after the tool row (both
stamped before the tool executed), the webview attached that row to the final
answer, and collapseCompletedWork used the answer's earliest attached
reasoning timestamp as the end anchor - excluding the entire tool execution
(e.g. 'Worked for 5s' for a turn with an 8s command).

- webview: end the work span at the answer row's own timestamp, clamped to
  the last collapsed row so a fallback answer bubble with a synthetic early
  timestamp cannot shrink the duration either
- sidecar: flush pending thinking before a tool_use row so rehydrated
  transcripts keep the live-stream order (thinking before its tool call) and
  pre-tool reasoning no longer rides on the next answer

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep interleaved thinking between the tool calls it separates

Address Greptile review: when one assistant message interleaves thinking
between multiple tool_use blocks, each reasoning segment now projects at its
own position (attached to a text row from its own segment when present,
otherwise as its own row) instead of merging into the first reasoning row,
which displayed later thinking before a tool call it actually followed.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): remove settings gear hover state while Account screen is open (#13408)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't show "No sessions found" while session history is still loading (#13414)

* fix(desktop): don't show 'No sessions found' while session history is still loading

Replace the isLoadingHistory flag with hasLoadedHistory, set only once the
backend has actually answered a list_discovered_sessions request. The sidebar
and Sessions view now keep their loading state until that first definitive
response, so the empty-state copy can no longer appear while history is still
being fetched (or while a failed fetch is being retried).

Also retry a failed initial fetch on the 2s event cadence instead of stranding
the UI until the 12s periodic poll, which is what stretched the misleading
empty state to ~10 seconds after a webview reload when the websocket lost the
race with the page load.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop history fast-retry from re-arming after hook unmount

A failed initial fetch that settles after the hook unmounted could schedule a
new retry timer after cleanup had already cleared the refs, leaving the
abandoned hook polling the backend every 2s. Guard scheduleRefresh with a
disposed ref set by the mount effect's cleanup.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix @ file mentions breaking on paths with spaces (#13391)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with "command not found" on VS Code 1.134 (#13402)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with 'command not found' on VS Code 1.134

Code action commands carried arguments (expandedRange, diagnostics),
which routes them through VS Code's CommandsConverter cache. VS Code
1.134 disposes the cached entries before the clicked action executes,
so every lightbulb action failed with 'Actual command not found,
wanted to execute cline.addToChat'.

Drop the arguments so the command id is passed through directly, and
recover the context in the handler instead: getContextForCommand now
expands an empty selection by 3 surrounding lines (matching the old
provider behavior) and gathers document diagnostics intersecting the
range when none are passed explicitly.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Scope gathered diagnostics to the selection/cursor

Match the old CodeActionContext.diagnostics behavior: only include
diagnostics intersecting the range the action was requested for, not
the surrounding lines the text gets expanded to.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop: unify Plugins, MCP, and Skills into one Plugins hub with a dedicated Marketplace page (#13411)

* Unify desktop plugins, apps, MCP, and skills into one Plugins hub with a Browse directory mode

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Open the marketplace directory as a modal over the Plugins hub instead of swapping the page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename directory to Marketplace: Browse Marketplace button, Marketplace modal title with icon, search placeholder

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix search input focus ring clipped by the Marketplace modal scroll container

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Address Greptile review: keep selected tag chip visible when its count drops to zero, and remount installed tab when a marketplace install completes after the modal closed

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Track marketplace modal mutation flag in a ref so a close click racing a queued render cannot skip the inventory remount

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make Marketplace its own settings page under Customizations and restore Channels as a standalone page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Remove icon from Marketplace page header for consistency with other settings pages

* Notify mounted inventory views when the marketplace invalidates the cache so late install completions refresh the Plugins hub

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop/ui): recommended and free model tiers in the composer model selector (#13410)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK (#13415)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): recommended-feed badges and descriptions in provider settings (#13416)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* feat(desktop): recommended-feed badges and descriptions in provider settings

Review suggestion on #13410: the provider settings page has room for
more model detail than the composer's picker. The cline/cline-pass
provider cards now refresh their model list through
list_provider_models (the catalog snapshot deliberately skips the
recommended-feed overlay so the startup catalog fetch never blocks on
the feed) and render Recommended/Free tier badges plus feed tags (NEW)
next to the model name, with the model description underneath. The
refreshed list also surfaces the live entries instead of the bundled
snapshot.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): scope the settings featured model list to its provider and revision

The fetched featured list was unscoped component state: switching
between cline and cline-pass reused the component instance, so the
previous provider's models stayed visible while the new request was
pending (or forever, when it failed), and the retained copy shadowed
later provider.modelList updates — adding a second custom model
submitted the stale list as the complete configuration and dropped the
first addition.

The fetched list now only applies to the provider and modelList
revision it was fetched for (falling back to the catalog snapshot
otherwise and refetching on membership changes), and add-model submits
the union of the displayed and configured ids so an update can never
silently unconfigure existing entries.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): update packed-Tailwind smoke contract for the picker's max-h-64 (#13421)

The ui-publish smoke check pins a set of Tailwind candidates the packed
sources must emit; #13410 grew the SearchCombobox options list from
max-h-56 to max-h-64, so the publish run failed on the stale candidate.
All other pinned candidates verified against the current sources.

* feat(desktop): refresh app icons and branding (#13400)

* ci(vscode): upload E2E failure recordings from the right path (#13427)

The job sets working-directory: apps/vscode, but that default applies to run
steps only, not to `uses:` steps. Since #10961 moved the extension under apps/
and added that default, the artifact path has resolved against the repo root,
matched nothing, and every failing run logged "No files were found with the
provided path: test-results/playwright/" instead of uploading recordings.

Widen to test-results/ so Playwright's error-context snapshots ship alongside
the videos.

* fix(hooks): deliver tool hook contextModification to the model (#13297)

* fix(hooks): deliver tool hook contextModification to the model

On the next engine, a tool_call (PreToolUse) hook's contextModification
was parsed into HookControl.context and then silently dropped: the
runtime beforeTool/afterTool result contract had no channel for
injecting conversation context. Legacy consumed it (ToolExecutor /
ToolHookUtils pushed <hook_context> blocks into the next user turn), so
this was a regression of documented behavior.

- Add appendContext to AgentBeforeToolResult/AgentAfterToolResult.
- AgentRuntime collects appendContext across hooks during an
  iteration's tool executions and appends one <hook_context> user
  message after the tool results, keeping tool-result parts contiguous.
- Map HookControl.context into appendContext in both subprocess hook
  layers (skipped when the hook cancels, matching legacy, where the
  message doubled as the error).
- Truncate injected context at 50KB per hook output, matching legacy.
- Concatenate appendContext across merged hook layers.

tool_result (PostToolUse) hooks still run detached with stdout ignored;
making them blocking so their context can be collected is a follow-up.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): stamp tool identity on injected hook context blocks

Contexts are batched into one message after the tool results, and
parallel tool execution collects them in completion order, so position
alone cannot attribute a block to its tool call. Add tool_name and
tool_call_id attributes to each <hook_context> block.

* fix(hooks): sanitize hook context block markup

Attribute values (tool_name, tool_call_id) are stripped of quote/angle
characters and embedded </hook_context> closers in hook output are
neutralized, so neither provider-supplied ids nor hook text can corrupt
or spoof a block's stamped identity.

* fix(hooks): neutralize forged opening hook_context tags in hook output

The previous sanitization only neutralized closing tags, so hook output
could still open a forged <hook_context> block claiming another tool's
identity. Escape both opening and closing embedded tags with one rule.

* fix(hooks): hide injected hook context from user-facing transcripts

Stamp the injected hook-context user message with displayRole 'system'
(the compaction-summary convention) so it reaches the model but does
not render as a user bubble in live or replayed transcripts. Without
this, resuming a session showed the raw <hook_context> block as if the
user had typed it.

* fix(hooks): neutralize case-variant embedded hook_context tags

The tag-neutralization regex was case-sensitive, so hook output could
still smuggle a forged tag as <HOOK_CONTEXT>. Match case-insensitively.

* fix(vscode): map PreToolUse contextModification into runtime appendContext

The extension's hooks adapter bridged file hooks into the SDK runtime
but forwarded only cancel/errorMessage, so a PreToolUse hook's
contextModification never reached the model. Map it into the runtime's
appendContext channel; HookFactory already truncates it at 50KB.

* fix(vscode): hide hook-injected context from replayed transcripts

Live sessions never rendered the injected <hook_context> user message,
but session reload replayed it as a user bubble (and post-resume turns
kept doing so). Treat these messages as synthetic in the user-message
mapping: honor the displayRole 'system' stamp the runtime sets, with a
text-prefix guard for paths where metadata is unavailable. This also
keeps edit/regenerate ordinal mapping aligned with visible bubbles.

* fix(hooks): run file hooks through exactly one layer per host

The VS Code extension registered two independent hook execution layers:
its own hooks adapter (config.hooks) and the SDK core's file-hook
extension from the runtime bootstrap. When both discover the same hook
files, every hook executes twice per event — and with context injection
wired, each contextModification would be injected twice.

Add a 'hooks' runtime config extension kind (in the default set, so the
CLI keeps core file hooks unchanged) and gate the bootstrap's file-hook
extension on it. The extension excludes 'hooks' at session start, so
its adapter — which also provides the hook status UI and the
hooksEnabled setting — is its single execution path.

* fix(vscode): discover hooks from the session workspace, not only global state

Hook discovery read workspaceRoots from global state shared across
every Cline instance, so another window repointing it made workspace
hooks silently stop being discovered. With the extension's adapter now
the single hook execution layer, that meant no hooks at all.

HookFactory takes an optional sessionWorkspaceRoot and unions that
root's .clinerules/hooks into discovery (and into cwd resolution), fed
from the session config's cwd. Shared-state discovery still works, so
behavior in the single-window case is unchanged.

* fix(hooks): keep sanitized hook attribute values distinguishable

Replacing every markup delimiter with the same underscore could
collapse two tool call ids that differ only by such a character into
identical stamps. Escape each delimiter with a distinct token instead.

* fix(hooks): make hook attribute sanitization injective

Escaping the underscore itself turns the attribute escaping into a
uniquely decodable code, so no two distinct tool call ids can collapse
to the same sanitized stamp (previously an id containing a literal
escape token could collide with an id containing the delimiter).

* fix(vscode): reconstruct hook status rows when replaying transcripts

hook_status messages are emitted live but never persisted, so reloading
a session dropped every hook row. The injected <hook_context> blocks
carry the hook source and tool name, so the replay translator now
rebuilds a completed hook status row from each block. The injection is
also no longer treated as a user turn boundary, so the final turn's
completion retag is unaffected by it.

* fix(hooks): collect PostToolUse hook output and honor its control (#13298)

* fix(hooks): collect PostToolUse hook output and honor its control

tool_result (PostToolUse) hooks ran fire-and-forget with stdout
ignored, so their entire JSON output — contextModification and cancel —
was discarded. Legacy awaited PostToolUse, injected its
contextModification into the conversation, and honored cancel.

- Run tool_result hook commands blocking (same 120s default timeout as
  tool_call) in both the hook-config-file layer and the agent-hook
  subprocess layer.
- Map their output: cancel stops the run with the hook's error message
  as the reason; otherwise context is injected via afterTool
  appendContext.

This restores legacy blocking semantics: tool results now wait for
tool_result hooks, but only in sessions that have one configured.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): bound tool_result hook wait and isolate cancel reason

Address review findings:
- The agent-hook subprocess layer forwarded an unset timeoutMs
  unchanged, so a tool hook command that never exits would block the
  agent indefinitely. Default both tool_call and tool_result to the
  120s bound the hook-config-file layer already used.
- A cancelling hook's error message was folded into the same context
  field as other hooks' injectable context, so merging controls could
  leak unrelated hook context into the cancellation reason. Carry it as
  a separate cancelReason, and surface it as the stop reason for
  beforeTool cancels too.

* fix(hooks): prefer errorMessage as a cancelling hook's stop reason

When a cancelling hook returns both contextModification and
errorMessage, the context-first parse precedence made the injectable
context the cancel reason and discarded the actual error. Parse the two
fields separately: errorMessage wins as the cancel reason (matching
legacy), and a lone errorMessage still folds into injectable context
for non-cancelling hooks as before.

* fix(vscode): honor PostToolUse hook cancel and contextModification

The adapter awaited PostToolUse hooks but discarded their output
entirely. Map cancel to a stop control (with errorMessage as the
reason) and contextModification into the runtime appendContext channel,
matching the PreToolUse mapping and legacy semantics.

* fix(hooks): whitespace-only errorMessage no longer suppresses the cancel reason

A cancelling hook returning meaningful context alongside a blank
errorMessage lost both: the parsers selected the whitespace as the
reason and the result mappers trimmed it away. Require a non-blank
errorMessage before it wins, so context serves as the fallback reason.
Apply the same fallback in the extension adapter's stop mapping.

* fix(core): stop Windows CI worker crashes from the agenda spec watcher (#13428)

* fix(core): watch agenda task specs via the resolved long path

fs.watch on a path with 8.3 short components (e.g. C:\Users\RUNNER~1
temp dirs) trips a libuv assertion in fs-event.c on Windows and aborts
the whole process. Since the agenda task manager landed, every hub
server test spins up its spec watcher on such a path on hosted Windows
runners, killing the vitest worker and failing the sdk-test Windows job
on every branch. Resolve the specs dir with realpathSync.native before
watching so libuv only ever sees the long form.

* test(ui): stub ResizeObserver for @pierre/diffs in tool-diff tests

jsdom does not implement ResizeObserver, so every ToolFileDiff render
logged a ReferenceError from @pierre/diffs to stderr. Tests still
passed; this just silences the noise the same way the constructable
stylesheet shim does.

* fix(core): skip the agenda spec watcher when the dir does not resolve

Falling back to the raw path on realpath failure would reintroduce the
Windows short-path abort; log and go without the watcher instead.

* fix(vscode): honor the classic truncation range when migrating legacy tasks (#13419)

Classic Cline truncated long conversations by omitting an index range of
api_conversation_history from every API request (keep the first
user-assistant pair, drop everything through the range end, strip
orphaned tool_results from the first kept message). The range was
persisted on the history item while the full history stayed on disk.

legacyApiHistoryToSdkMessages ignored conversationHistoryDeletedRange
and converted the entire file, so resuming a migrated long task handed
the SDK an untruncated working context that could exceed the model's
context window by millions of tokens - every request failed with
'prompt is too long' and every compaction restarted from the full
history (#12996, confirmed by the reporter: the task was migrated from
an older version and broke after a restart, with each compaction
starting from ~3M tokens).

The migration now replays exactly what the classic extension sent:
slice out the deleted range and drop orphaned tool_results, mirroring
ContextManager.getTruncatedMessages (see origin/main). Malformed ranges
fall back to the full history (previous behavior).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): show the diff edit view for multi-line edits in CRLF files (#13417)

The edit preview computed proposed content with an exact old_text match, but
the SDK executor normalizes old/new text to the file's own line endings before
matching (#12305) - reads strip CR, so models emit LF-only text even for CRLF
files. Any multi-line old_text in a CRLF file therefore failed the preview's
match: the diff edit view silently never opened while the executor applied the
edit. Single-line edits (no line break in old_text) were unaffected, which is
why the diff view appeared to trigger inconsistently.

Mirror the executor's EOL normalization (and its literal $-sequence insertion)
in the preview computation.

Fixes #13296

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): report truthful session status so desktop checkpoint restore stops wedging (#13418)

* fix(core): keep hub session status truthful across queue-drained turns

Queue-drained turns settle only through the event stream, but the hub
runtime host mistranslated their lifecycle in two ways:

- session.updated events carrying only a snapshot (persistence updates)
  defaulted the projected status to "running". When one trailed the
  final idle update after a turn, clients that track busy state from
  status events (the desktop sidecar's workspace restore gate) stayed
  busy forever. Use the snapshot's real status and emit nothing when
  neither source reports one.
- the per-run agent.done dedup was only reset by run.started, which the
  daemon-side queue drain never publishes, so a drained turn's done was
  swallowed as a duplicate of the previous turn's. Reset the dedup on
  session.pending_prompt_submitted, and suppress stale run.completed
  events that land inside a drained turn's window so they can neither
  emit a phantom done nor consume the drained turn's dedup slot.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(desktop): cover restore unlock after an event-settled queued turn

Exports the sidecar's core-session event handler so the queued-turn
lifecycle (busy via status events, cleared by the done agent event,
restore allowed afterwards) is testable end-to-end.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): start interactive sessions without a prompt as idle

The runtime host reported every new session as "running" until its
first turn ended. Interactive hosts (the desktop app) start sessions
with no prompt and dispatch turns through separate send calls, so a
created-but-never-prompted session stayed "running" forever — wedging
clients that gate workspace operations (checkpoint restore, message
edit) on active turns.

Interactive no-prompt starts now begin idle, start emits the session's
actual status (resumed sessions no longer masquerade as running), and
markTurn* transitions keep tracking in-memory status for lazily
persisted sessions so the first turn still reports running -> idle.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: format hub-runtime-host test filter

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop the drained-turn done bookkeeping, keep the minimal fix

The stuck restore is fully explained by the two status defects (fabricated
"running" from snapshot-only session.updated events, and never-prompted
interactive sessions reporting "running"). The done-dedup machinery for
queue-drained turns addressed a separate cosmetic gap (queued turns emit no
chat_done, pre-existing) and required fragile run-window heuristics, so it
is removed to keep this change reviewable. Sidecar test now settles the
queued turn through the status event, matching the shipped mechanism.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs(sdk): document the truthful session-status contract

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(deps): update Langfuse packages and bump app versions (#13443)

* chore(deps): update Langfuse packages and bump app versions

Update @langfuse/otel to v5.10.1 and add @langfuse/vercel-ai-sdk v5.9.1 for improved observability with Vercel AI SDK.

Bump versions for @cline/code to 0.0.14 and @cline/ui to 0.2.0-next.6, updated via bun.lock.

Other Changes:
Added optional userId to AgentRuntimeConfig.
Propagated userId, sessionId, conversationId, runId, iteration, provider, and model context into AI SDK telemetry.
Added AI SDK 7 runtimeContext with explicit includeRuntimeContext.
Added stable OTEL_SERVICE_NAME=cline-sdk.
Added runtime metadata assertions in agent tests.

* add taskId

* Revert "add taskId"

This reverts commit f20d31d96d.

* docs: simplify Open Cline step in installing guide (#13405)

* docs: remove duplicate GLM-5.3 rows in ClinePass tables (#13449)

Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>

* chore(sdk): release v0.0.76

* chore(cli): release v3.0.56

* docs(cli): scope the v3.0.56 release notes to CLI-visible changes

* feat(desktop): interactive welcome hero graphic (#13399)

* feat(desktop): add interactive welcome hero

* feat(desktop): support composable welcome hero variants

* feat(desktop): reskin first-run onboarding (#13441)

* refactor: centralize client tool availability (#13451)

* chore(sdk): release v0.0.77

* docs(cli): drop the tasks tool from the v3.0.56 notes, it is desktop-only

* chore(vscode): prepare 4.1.11 release

* chore(desktop): release v0.0.15

* fix(vscode): remote config MCP settings (#13466)

* fix(vscode): enforce enterprise MCP controls on the Customize marketplace

The unified Customize marketplace replaced the old MCP marketplace
without carrying over enterprise remote-config enforcement: the catalog
RPC returned every MCP entry and installs were never policy-checked,
so orgs with mcpMarketplaceEnabled=false or an allowedMCPServers
allowlist saw (and could install) all marketplace MCP servers.

- Filter MCP entries out of getMarketplaceCatalog when the marketplace
  is disabled, and restrict entries to the allowlist when configured
  (matching entry id, display name, installed server name, or source
  repo URL, mirroring legacy GitHub-URL allowlist ids)
- Reject installMarketplaceEntry requests that violate the policy
- Map the published catalog's repo/homepage fields onto
  sourceUrl/homepageUrl so URL-based allowlists can match
- Update the enterprise MCP server controls docs

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: simplify MCP marketplace policy enforcement

Fold the policy check into marketplace-helpers, drop the dedicated
test suite, and trim the docs edit to the strictly necessary line.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Treat an empty preserved capability list as unspecified when seeding tools (#13465)

* Treat an empty preserved capability list as unspecified when seeding tools

toSdkModelInfo guarded the tools seeding with a strict
preservedCapabilities === undefined check, but modelHasCapability —
the runtime's own reader — treats undefined AND length === 0 as
"unspecified". A custom OpenAI-Compatible model whose stored
capabilities field is a defined-but-empty array (a config carried over
from before the field existed, or one round-tripped through a boundary
that defaults it to []) skipped the seeding; the first boolean
projection to run afterwards (e.g. supportsReasoning) then populated
the array, the runtime gate read the non-empty, tool-less list as
authoritative, and every tool definition was silently dropped from the
session (#13463).

The guard now covers the empty array too, matching the reader's
unspecified semantics.

* test: satisfy the store's isModelInfo gate so the empty-capabilities case actually reaches knownModels

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: yzxcj797 <yzxcj797@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>

* chore(vscode): prepare 4.1.12 release

* Add feature flags to the desktop app (#13289)

* Add feature flags to the app

* React to account updates

* Address comments

* use a per-app file

* fix: propagate Langfuse session telemetry (#13473)

* fix telemetry session propagation

* feat telemetry client version metadata

* fix(core): address Langfuse review feedback — hub client identity + delegated agent session grouping (#13475)

* fix(core): rebuild hub session client identity from request headers

Hub-backed sessions do not transport extensionContext (it is local-only),
so the daemon's runtime built traces without the clientName/clientVersion
metadata even though the hub client bakes X-CLIENT-TYPE / X-CLIENT-VERSION
into the session's provider headers. Reconstruct extensionContext.client
from those headers during local runtime bootstrap so hub-backed Langfuse
traces carry the same client identity as local runtimes, and the daemon's
header re-resolution stops clobbering the original X-CLIENT-TYPE.

* fix(core): propagate parent distinctId/sessionId to delegated agents

Delegated agents (spawned sub-agents, configured agents, teammates) were
built without distinctId and sessionId, so their Langfuse traces had no
userId or sessionId and did not group with the parent user or session.
Thread the host-resolved distinctId through RuntimeBuilderInput and the
root sessionId through the delegated-agent config provider, and copy both
onto the delegated AgentConfig.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* ci(vscode): make combined nightly manual-dispatch only

The PublishNightly environment gained required reviewers, so each cron
run parked on approval, held the workflow's concurrency group, and
silently cancelled every scheduled run queued behind it. 20 consecutive
scheduled nightlies died this way between 2026-07-31 and 2026-08-21;
the only nightlies that shipped in that window were manual dispatches.

Drop the cron rather than leave a trigger that cannot succeed unattended.

* feat(hub): add drain and upgrade commands with replay support (#13468)

* feat(hub): add drain and upgrade commands with replay support

* handles disconnection

* feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport

Completes the wiring the previous commits' primitives needed:
HubServerTransport gains isDraining(), hub.drain/hub.status/profile.get
command handling, and replayEventsAfter() (backed by the durable event
log), plus the sequence/sinceSequence wire types they depend on in
shared/hub.ts. run-queue-handlers.ts reads the active bot profile's
plugin roots when executing durable runs.

Also adds hub/profiles/: profile.json (identity/rules/plugins) ->
system prompt composition, --profile / CLINE_HUB_BOT_PROFILE
resolution, and the bundled cline-dad profile with its
cline_hub_support read-only diagnostics tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Revert "feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport"

This reverts commit 6696d5d202.

* fix(hub): dedupe replayed events by eventId, not just sequence

HubEventLogStore.append() returns a new envelope stamped with a
sequence rather than mutating the input, so a pending approval
re-issued sequence-less by subscribe() (it predates any durable-log
append) and its later sequence-stamped copy from the durable log are
two different objects carrying the same eventId. The replay-then-live
buffer in browser-websocket.ts only deduped by sequence, so the
sequence-less copy's guard never tripped and it was delivered a second
time when the buffer flushed after replay.

Track delivered eventIds alongside the sequence cursor; eventId
survives the append/stamp round-trip unchanged, so this dedupes the
exact-same logical event regardless of which copy arrives first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): wire drain, durable event log, and run queue into the live transport

CI on this branch failed bun run build:sdk: browser-websocket.ts,
client/index.ts, and hub-websocket-server.ts (already on this branch)
reference sequence/sinceSequence, HubServerTransport.isDraining(), and
the "hub.drain" command — but the commit that reverted bot profiles
out of this branch also reverted this wiring, since it shared a commit
with the profiles work. That wiring is a hub concern, not a
bot-profiles one; split it back out.

- shared/hub.ts: sequence/sinceSequence types, run.enqueue/run.list/
  hub.drain/hub.status/stream.replay capability, command, and event
  names. profile.get intentionally excluded — stays bot-profiles-only.
- context.ts: isDraining() on HubTransportContext. botProfile field
  intentionally excluded.
- hub-server-transport.ts: eventLog/runQueue fields and start/stop
  lifecycle, publish() appends to the durable log, handleCommand cases
  for run.enqueue/run.list/hub.drain/hub.status, drain-refusal check,
  replayEventsAfter()/lastEventSequence(). startBotProfile()/
  startHubSupportTool() and the profile.get case intentionally
  excluded.
- run-queue-handlers.ts: added without handleProfileGet (needs
  ctx.botProfile, which doesn't exist here).
- hub-upgrades.test.ts: added without its two bot-profile-injection
  tests (they need a resolved bot profile to assert against).

Verified bun run build:sdk exits 0 (the exact CI command) and
bunx vitest run src/hub passes (311/312; the one failure is the
same pre-existing environment-timing flake already present before
this change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): export instance-lock, event-log, and run-queue from the hub barrel

These landed as internal modules only; hub-server-transport.ts and
hub-websocket-server.ts import them by direct path, but nothing
re-exported them from the public @cline/core/hub surface the way
sibling discovery/server modules already are.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): wire the instance lock into the daemon entry point

The singleton lock (discovery/instance-lock.ts) and its consumption in
startHubWebSocketServer/ensureHubWebSocketServer were already on this
branch, but the daemon entry point's own half was not: retrying a bind
when a retiring predecessor still holds the lock, and exiting with a
distinct code (3) instead of the generic fatal path when a live Hub
already owns the data directory. Without this, a daemon racing a
retiring predecessor could fail outright instead of waiting the lock
out, and losing the singleton race looked identical to a crash.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): address drain/upgrade review findings (#13478)

- cline hub upgrade: check idleness at least once (--wait 0 works), reject
  non-numeric --wait, and un-drain on every abort path so an aborted
  upgrade can never leave the hub refusing new work
- add cline hub drain --off and the off query param to requestHubDrain so
  POST /drain?off is reachable from shipped code
- HubEventLogStore/HubRunQueue: WAL journal mode + busy_timeout, and stamp
  sequences from lastInsertRowid instead of SELECT MAX(sequence)
- HubInstanceLock.acquire: degrade to an unheld lock when SQLite is
  unavailable instead of refusing hub startup; only BUSY/LOCKED still
  raises HubLockHeldError
- ensureHubWebSocketServer: retire an unusable discovered hub through the
  shared retireDiscoveredHub (busy hubs are attached to, drain precedes
  shutdown, discovery cleared only when the hub actually retired)
- replay adapter: advance the cursor past eventId-deduped events, cap
  replay pages, stop when the cursor stalls, and drop the dedupe set after
  the buffered flush so it cannot grow for the socket lifetime

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* fix(hub): derive the singleton e2e challenger cwd portably

The challenger's working directory was derived by round-tripping the
discovery path through a file: URL and stripping the last pathname
segment. On Windows that yields a POSIX-style '/C:/...' path, which is
not a valid spawn cwd, so the spawn fails ENOENT before the singleton
lock is ever contested and the Windows SDK test job goes red.

The data dir is simply the discovery file's parent: use dirname().

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop stored capability lists from silently revoking tool calling for custom models (#13476)

* fix(core): seed tools capability when custom model capabilities are synthesized from boolean flags

For a models.json entry with no explicit capabilities list, toStoredModelInfo
synthesized a capability array purely from boolean convenience flags (e.g.
supportsReasoning: true -> ["reasoning"]). modelSupportsToolCalling fails open
only for a missing or empty list, so the synthesized non-empty list read as an
authoritative denial and silently stripped every tool definition from requests
to custom OpenAI-compatible models (#13463).

Seed "tools" whenever the list was not explicitly authored and the boolean
projections made it non-empty, preserving the fail-open contract. Explicitly
authored capability lists remain authoritative and can still disable tools.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(core): cover stale catalog capability overrides

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: treat stored capability lists as non-authoritative for tool calling

The hasExplicitCapabilities guard still let two producers of tool-less
lists through:

- The VS Code legacy-override migration (legacyModelInfoToOverrides)
  persists explicit partial lists like ["prompt-cache"] into models.json
  for custom OpenAI-compatible models, which then read as an authoritative
  "cannot call tools" and drop every tool - same symptom as #13463.
- Any hand- or UI-authored partial list on a non-catalog model.

Stored entries and user-authored provider metadata have no way to declare
"cannot call tools" (there is no supportsTools field, and every writer
that authors a full list includes "tools"), so seed "tools" into any
non-empty list for a language model. Only generated catalog capabilities
remain authoritative - a genuine no-tools catalog model stays that way -
and non-language models (e.g. image generation) never gain a tools claim.

Also make legacyModelInfoToOverrides write "tools" into the arrays it
fabricates, matching the providers.json migration, so models.json stops
being poisoned for older readers.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): prepare 4.1.13 release

* chore(sdk): release v0.0.78

* chore(cli): release v3.0.57

* fix(core): run hub e2e files serially so daemon timing budgets survive CI contention

singleton.e2e.test.ts (added in #13468) spawns real daemons and runs for
~15s. Vitest's default file parallelism let it run alongside
shutdown.e2e.test.ts, whose assertions are wall-clock bound: discovery
within 10s, exit within 5s, and a 2s shutdown watchdog. On the 2-core
windows-latest runner that contention alone broke those budgets, failing
the shutdown test two different ways across runs — once never observing
discovery, once with the daemon forced to exit before its HTTP 202
flushed (socket hang up). The test passed on Windows before #13468 and
has failed every SDK publish run since.

* chore(desktop): release v0.0.16

* test(sdk): give windows-sensitive suites realistic timeouts

Four consecutive SDK publish runs failed on windows-latest, each on a
different test, all of them plain timeouts: two @cline/shared SQLite
tests at the 5s vitest default, core's bash executor at 10s, and the hub
singleton endpoint test at 10s. The 2-core Windows runner spawns forks
and takes SQLite locks slowly enough to blow those budgets under load.

These timeouts guard against hangs; they are not timing assertions (the
one suite that does assert elapsed time, shutdown.e2e, was fixed by
removing file-level parallelism instead). Raise core to 20s and give
@cline/shared an explicit 15s in place of the inherited 5s default.

* fix(telemetry): emit task.completed from every session teardown path (#13489)

The task.completed fallback lived only inside shutdownSession, but
stopSession/dispose route interactive sessions with a terminal reported
status through releaseSessionRuntime, which never emitted. Truthful
session-status reporting (shipped in 4.1.11) re-routed a large share of
interactive stops onto that branch and silently dropped the event.

Route the emission through a single choke point,
emitTaskCompletedOnTeardown, called from both shutdownSession and
releaseSessionRuntime. The completion criterion no longer reads
session.status: interactive sessions use the recorded final-turn
outcome (lastInteractiveTurnFinishReason), non-interactive sessions
keep the existing input.status === "completed" logic. A new
taskCompletedEmitted flag (also set by the submit_and_exit observer)
enforces exactly one task.completed per session. failSession now
records the errored final turn so a stale "completed" from an earlier
turn can never leak into the teardown emission. Telemetry only; no
user-facing behavior changes.

* chore(vscode): release v4.1.14

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on (#13498)

* fix(vscode): honor MCP auto-approve settings for SDK tool calls

The SDK extension required both the global 'Use MCP servers' auto-approve
toggle AND each tool's per-tool autoApprove flag before silently approving
an MCP call, while the legacy extension treated them as either/or. Restore
the legacy OR semantics so toggling MCP auto-approve works again.

Also key toolPolicies by the registered SDK tool name (via
defaultMcpToolNameTransform, now exported from @cline/core) instead of raw
server__tool. Servers whose names contain sanitized characters (e.g.
marketplace names like github.com/user/repo) or exceed 64 chars produced
policy keys that never matched the registered tool, so those MCP tools ran
without any approval gate; the live auto-approve lookup now re-applies the
transform instead of string-splitting the name.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Revert "fix(vscode): honor MCP auto-approve settings for SDK tool calls"

This reverts commit 86c568fbba.

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on

The SDK extension only auto-approved an MCP call when the global 'Use MCP
servers' auto-approve toggle AND that tool's per-tool autoApprove flag were
both set, so toggling MCP auto-approve appeared to do nothing and users had
to opt in each tool individually. The toggle alone now governs all MCP
tools; the per-tool flag is no longer consulted.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): release v4.1.15

* fix(cli): remove the $4.99 ClinePass promo copy (#13514)

The $4.99 first-month promo is ending, so the CLI's first-launch "Try ClinePass" dialog should no longer advertise it. Also drops the leftover CLI_PROMO_CODE plumbing, which has been an empty string since the promo-code flow was removed.

* fix(vscode): resolve hook workspace identity from the window, not shared global state (#13352)

* fix(vscode): resolve hook workspace identity from the window, not shared global state

Hook discovery, hook cwd selection, and the workspaceRoots metadata passed
to hook scripts all read the workspaceRoots/primaryRootIndex global state
keys. Global state lives in ~/.cline and is shared by every Cline instance
(all VS Code windows, the CLI, the JetBrains plugin), and nothing writes
these keys anymore, so hooks resolved against whatever project some other
or older instance last recorded. With a second window open on another
project, a workspace's .clinerules/hooks scripts were never discovered.

Resolve workspace roots via a single guarded helper backed by
HostProvider.workspace.getWorkspacePaths() (in-process, window-scoped,
same as refreshHooks): blank paths are filtered, a host-bridge failure
degrades to no workspace roots instead of silently disabling global hooks
or skipping blocking PreToolUse guards, and one resolution is threaded
through hooks-dir discovery, cache misses, cwd selection, and hook input
metadata so they can't disagree (previously up to four host lookups per
hook execution — real gRPC round trips in the standalone host). Roots and
hooks dirs are matched on whole path segments with the longest root
winning, so prefix-sharing or nested workspace roots resolve to the right
project. The adapter creates the runner once per event and skips no-op
runners, making creation the single resolution point; the separate
hasHook/getHookInfo checks are removed. The dead workspaceRoots and
primaryRootIndex state keys are dropped, and the four hand-rolled
HostProvider.workspace test stubs are consolidated into one shared
helper.

* test(vscode): add e2e coverage for workspace-scoped hook discovery

Boots real VS Code with the packaged extension against the workspace
fixture, sends a prompt, and asserts the fixture's UserPromptSubmit hook
was discovered from the open window's workspace, executed with that
workspace root as its cwd, and received the same root in its
workspaceRoots input — the end-to-end contract the hook workspace
identity fix establishes.

* test(vscode): isolate the e2e hook fixture from the shared workspace

The UserPromptSubmit fixture hook lived in the shared e2e workspace, so
every prompt-sending spec executed it (hooksEnabled defaults to true) —
and its cold PowerShell spawn on Windows pushed chat.test.ts past the
5s expect timeout. hooks.test.ts now overrides workspaceDir to a
dedicated workspace-hooks fixture, so only the hooks spec pays the hook
spawn.

* fix(hub): cap hub-events db size so it can't fill the disk (#13516)

* fix(hub): cap hub-events db size so it can't fill the disk

Row/time retention alone didn't bound disk usage: envelopes carrying
full session snapshots reach hundreds of KB each, so retained rows
could total tens of GB, sweeps only ran hourly, and DELETE never
shrinks a SQLite file. Enforce a 64 MiB size budget in prune() (oldest
rows first, VACUUM to return the space), and also prune after every
16 MiB appended so bursts can't outrun the hourly timer.

Fixes #13505

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): tolerate VACUUM failure on a full disk

VACUUM needs scratch space and can fail in exactly the state a
ballooned event log causes. The byte-budget deletes already bound live
data, so swallow the error and let the next sweep retry the reclaim
instead of aborting startup pruning and disabling the durable log.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): count the size budget in UTF-8 bytes, not characters

envelopeJson.length (UTF-16 units) and SQLite LENGTH() (characters)
undercount multibyte text by up to 3x, which could leave a CJK-heavy
log settled above budget and re-running VACUUM every sweep. Use
Buffer.byteLength and LENGTH(CAST(... AS BLOB)) instead.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(sdk): carry root overrides into the Node smoke-test sandbox (#13517)

ci-node-smoke.ts installs the packed SDK tarballs with a plain npm
install in a fresh temp dir, where the repo root package.json overrides
do not apply. When @sap-cloud-sdk 4.9.0 shipped (2026-08-24) it broke
@sap-ai-sdk/ai-api 2.14.0 (via @jerome-benoit/sap-ai-provider in
@cline/llms) with ERR_PACKAGE_PATH_NOT_EXPORTED, failing the smoke step
on every PR even though the root already pins @sap-cloud-sdk/* to 4.6.0.

Copy the root overrides block into the generated sandbox package.json
so the smoke install resolves the same pinned versions as the repo and
future third-party releases cannot break it independently.

* chore(sdk): release v0.0.79

* fix(vscode): don't steal last-used provider from ClinePass on credential refresh (#13520)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): flush the /shutdown 202 before daemon teardown

The /shutdown handler queued teardown on a microtask, which runs before
the event loop's write phase, so the daemon could process.exit() before
the accepted 202 was handed to the socket. Unix masked it (uv_try_write
lands small loopback writes synchronously); Windows has no such fast
path and lost the race regularly — the recurring shutdown.e2e.test.ts
'socket hang up' failures on windows-latest. Start teardown from the
response's write callback instead, with an idempotent 1s fallback so a
client that vanishes mid-write cannot strand the daemon, and send
Connection: close so the client gets a FIN rather than an abort.

Since the flakiness this compensated for is fixed at the source, restore
maxWorkers: 2 for the Windows core suite (serializing it cost ~3 min of
CI per run), and raise the e2e daemon discovery hang guard 10s→30s —
it guards against hangs, not runner speed.

* chore(cli): release v3.0.58

* fix(core): prevent search_codebase from crashing the process on giant single-line files (#13525)

* fix(core): prevent search_codebase from crashing the process on giant single-line files

searchWithRipgrep buffered all of rg's --json stdout into one string. Each
JSON event embeds the full text of the matched line (--max-columns is
ignored in JSON mode), so searching a directory of serialized trace dumps
(single-line multi-hundred-MB JSON files) accumulated gigabytes of stdout
until string concatenation threw RangeError: Out of memory inside the
stream data handler. That throw is outside the tool's try/catch, so it
escalated to an uncaughtException and killed the CLI/hub daemon.

Parse rg's JSON events incrementally line by line, drop events larger
than 256KB, truncate matched/context lines to MAX_LINE_CHARS, and stop
reading once maxResults is reached. The fallback regex scan now skips
files larger than 10MB (reporting the skip count) and truncates its
context lines the same way.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* simplify search_codebase crash fix to a minimal diff

Replace the incremental JSON-event parser with three small guards: stop
buffering rg stdout past 10MB, drop the trailing partial event before
parsing, and slice fallback context lines to MAX_LINE_CHARS. Drops the
fallback file-size skip and skip-count reporting.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag (#13522)

* chore(vscode): remove per-tool MCP auto-approve checkboxes from webview

MCP auto-approval is now governed solely by the global 'Use MCP servers'
toggle; the SDK approval path (shared with the CLI and desktop app) has no
per-tool granularity, so the per-tool and 'Auto-approve all tools'
checkboxes were no-ops that implied control that no longer exists. Remove
them from the MCP settings view and chat tool rows. The autoApprove arrays
in cline_mcp_settings.json and the toggleToolAutoApprove RPC are left
intact for the legacy extension.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag

Keep the checkbox components, handlers, and RPC plumbing intact but gate
rendering behind SHOW_MCP_PER_TOOL_AUTO_APPROVE=false: the SDK approval
path (shared with the CLI and desktop app) is all-or-nothing via the
global 'Use MCP servers' toggle, so the per-tool checkboxes were no-ops.
Flip the flag back on if the SDK gains per-tool approval granularity.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(tools): create new files with the platform-native line ending (#13521)

* fix(tools): use platform-native EOL for new files and preserve CRLF in apply_patch updates

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* simplify to the minimal new-file EOL fix

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* extract shared normalizeNewFileLineEndings helper

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add suggested schedule templates to the desktop Schedules page (#13529)

* Add suggested schedule templates to desktop Schedules page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix unreadable selected text in inputs caused by selection utility conflict

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Restyle Suggested section label as small gray uppercase

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Hide suggested schedule cards that match an existing schedule name

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Disable the agent todo tool and hide the Agenda UI in the desktop app (#13530)

* remove todo tool and Agenda UI, keep schedule-only tasks tool

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: biome formatting fixes

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* restore agenda backend; disable todo kind behind a flag instead of deleting

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* keep agenda automation pump idle while the todo tool is disabled

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* remove todo tool and Agenda UI altogether (revert the disable-flag hybrid)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* restore all agenda code to main state

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* disable agent todo tool and hide Agenda UI behind flags

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add Desktop App and Cloud Platform to bug report issue template (#13532)

* Add Desktop App and Cloud Platform to bug report surfaces

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename Surface Diagnostics field to Diagnostics

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: sidebar navigation cleanup with New/Schedule/Customize rows and dialog-based search (#13533)

* desktop: clean up sidebar navigation chrome

- Give New Task its own full-width labeled row below the logo row
  instead of an ambiguous icon next to the agenda toggle
- Wire the New Task row to the home action so starting a new task
  clearly takes you home (the logo still works as a fallback)
- Swap back/forward chevrons for browser-style arrow icons and
  bump their size

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: sidebar New/Schedule/Customize rows and always-visible search

- Stack New (plus icon), Schedule, and Customize as full-width labeled
  rows below the logo; whole row highlights on hover via sidebarItem
- New starts a fresh task (home), Schedule opens Settings > Schedules,
  Customize opens the Customizations sections (Plugins first)
- Show the session search bar permanently above the sessions list
  instead of hiding it behind a search icon toggle

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: move session search into a dialog behind a logo-row icon

- Replace the inline sidebar search bar with a search icon in the
  logo row that opens a cmdk command dialog listing sessions
- Selecting a result opens that session and closes the dialog
- Remove the agenda/tasks toggle the icon replaces, along with the
  now-unreachable sidebar Agenda panel (the welcome screen still
  surfaces agenda tasks)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: load full session history when the search dialog opens

Addresses Greptile review on #13533: the dialog only searched the
currently loaded history batch, so older unloaded sessions could not
be found. Opening search now kicks off loadAllSessions() (the hook's
purpose-built global-search loader), and the empty state reads
'Searching older sessions...' while more history is streaming in.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Hide Channels and Agents sections from desktop app sidebar (#13527)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop app: organize sidebar sessions into Pinned, Scheduled, and Tasks sections (#13528)

* Add Pinned/Scheduled/Tasks categories to desktop app sidebar

Replace the Schedules and Favorites filter-menu options with visible
collapsible category sections in the session sidebar, and rename the
Favorite action to Pin across the sidebar and sessions view.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Grow full history window when Tasks show-more outpaces loaded tasks

loadMoreSessions treats its argument as a limit on all sessions, but the
Tasks show-more count only tracks Task rows, so once pinned/scheduled
rows pushed the loaded total past the requested count the call no-oped
and clicks went dead. Grow the whole history window via
loadOlderSessions instead, and only when the loaded tasks cannot fill
the next page. Addresses Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Auto-fill the Tasks page instead of fetching once per show-more click

A single 50-session window growth can consist entirely of pinned or
scheduled sessions, leaving a show-more click with no visible Tasks
progress. Replace the one-shot fetch with a page-fill effect that keeps
growing the history window until the requested Tasks page fills or
history runs out. Addresses the follow-up Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Halt page-fill retries after a failed history fetch

A failed fetch leaves the task count and has-more state unchanged,
which are exactly the conditions the page-fill effect fires on, so one
failing request would retry and re-toast forever. Halt the effect after
a failure and let the next explicit show-more click retry. Addresses
the third Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Redesign desktop Model Providers page and split voice input into its own settings page (#13531)

* Redesign desktop Model Providers page and split voice input into its own settings page

- Group providers into Connected / Popular / All with auth-kind hints and
  connection status instead of per-row enable toggles
- Show browser sign-in (not an API key field) for OAuth providers, with a
  collapsed manual-key escape hatch where supported, plus explicit
  Connect / Disconnect / Sign out actions
- Move voice input to a dedicated Settings > Voice page that only offers
  connected transcription-capable providers, preselects a default model
  (streaming preferred), and stays disabled in the sidebar until a
  provider is connected

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Show native tooltip on the disabled Voice settings nav item

Disabled buttons drop pointer events, so the 'connect a model provider'
hint moves to a wrapping span for the browser tooltip to render.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Drop letter avatars and gray provider ids from provider rows and voice chips

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Drop model counts from provider list rows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename provider Connected status to Configured and drop the green styling

A settings entry is configuration, not a live connection; neutral gray
text avoids implying an active link, since the user still picks which
configured provider to use per chat.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Resync provider catalog from disk when a settings save fails

Connect/disconnect/credential edits update the list optimistically; a
failed save now reloads the catalog instead of leaving the optimistic
state (and the view's module cache) claiming a configuration that was
never persisted.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename oauthProvider test fixture to dodge CodeQL name heuristic

CodeQL's clear-text-storage query flags any identifier matching 'oauth'
as a credential source and traced the fixture's provider id into the
favorite-models localStorage write, which stores only provider/model id
strings. Renaming the fixture removes the false-positive source.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Guard catalog reloads against races and resync detail drafts on failed saves

Optimistic provider mutations now bump a generation that discards any
in-flight catalog response, so a failed-save recovery reload can't
overwrite a newer edit with an older disk snapshot. The recovery also
remounts the provider detail panel via a reset token so its local field
drafts reflect the reloaded on-disk state instead of unpersisted edits
or an optimistically cleared disconnect.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix failed-save recovery ordering and retry superseded reloads

Remount the provider detail only after the authoritative catalog reload
lands, so its drafts re-seed from disk state rather than the optimistic
values that failed to persist. When a concurrent edit supersedes the
recovery's in-flight response, retry the reload (bounded) instead of
dropping it, since that edit performs no reload of its own.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): Customize hub, sidebar overhaul, and settings polish (#13538)

* feat(desktop): merge customization pages into a Customize hub with inline marketplace

Replaces the Plugins page and the dedicated Marketplace page with a single
Customize hub. Tabs: Skills, MCP, Plugins, Rules, Hooks, Tools, each with
live counts. Tabs backed by a marketplace catalog render the installed
items followed by an inline browsable Browse section (CLI-hub style), so
installing from the catalog immediately reflects in Installed above.

- Installed cards restyled to mirror the browse-card anatomy: bg-card p-4
  containers, absolute top-right xs Uninstall matching Install, truncating
  semibold titles, primary-tinted icons, real Badge components instead of
  ad-hoc bordered spans, un-indented line-clamped descriptions
- Rules/Hooks/Tools rows brought into the same card language; redundant
  intro paragraphs (duplicating the page description) removed; Tools group
  headers match the Installed header style with counts
- Marketplace section header renamed to Browse; duplicate 'N results' row
  removed (the header count is the single source)
- MCP embedded view now shows the full marketplace instead of
  installed-only

* feat(desktop): overhaul sidebar sessions and navigation

Sessions list:
- Sort toggle removed; sessions are always grouped by project, with pinned
  sessions leading each group (both subsets ordered by recency). The
  Pinned/Scheduled/Tasks category sections and their time-mode paging
  machinery (page-fill effect included) are deleted
- Scheduled sessions get an inline clock icon next to the pin position;
  pin + clock render together when both apply, and the running/unread
  status dot now coexists with them
- One font size (text-sm) across the list: titles, timestamps, project
  headers, show-more buttons, empty states. sidebarText needed !text-sm
  because the default button size's text-base wins the twMerge conflict
- Gradient fade under the Sessions header once the list scrolls, so rows
  fade out instead of hard-clipping
- The session-detail hover card is controlled from the sidebar and closes
  on scroll (Radix receives no pointer events while scrolling, so it used
  to float over moving content)
- Sidebar min resize width raised 224->260px; the per-project show-more
  label truncates so its nowrap text can't force rows to overflow and clip
  timestamps at narrow widths

Navigation:
- Customize replaces the Plugins/Marketplace/Hooks/Rules/Tools sidebar
  entries; Schedules and Customize are hidden from the expanded settings
  nav (their top rows cover them) but stay reachable when collapsed
- The settings gear always opens General instead of resuming the last
  section; the Account no-op hover special case is gone
- The New row highlights (aria-current) while the fresh not-yet-started
  task page is showing and hands off to the session row once the task
  starts; hitting New also focuses the prompt input via a window-event
  signal (lib/prompt-input-focus.ts) since the sidebar and composer sit in
  distant subtrees
- Fixed the xs button size collapsing any icon-bearing button to 12x12
  (leftover has-[>svg]:size-3 from when xs was a micro button) — this was
  why Uninstall buttons rendered broken next to Install

* feat(desktop): polish settings pages and chat composer

Models page:
- The provider detail panel is always open: no X button, no empty
  no-selection state. It defaults to the first connected provider (falling
  back to the first in the catalog), which also removes the layout shift
  that happened when the page swapped between full-width and panel
  variants on selection
- Fixed the list pane becoming unscrollable while the panel was open:
  grid items default to min-size auto, so the pane grew past its track
  inside the overflow-hidden grid and its ScrollArea had nothing to
  scroll; wrapped it in a min-h-0 min-w-0 cell
- Add Provider opens a Dialog instead of swapping the page
  (AddProviderContent gained a dialog variant that renders only the form)
- Embedded inputs (provider search, model search, detail fields) share one
  EMBEDDED_INPUT_CLASS stripping the Input component's own border/dark bg
  tint/shadow/ring, which rendered as a mismatched inner box; the model
  search box uses the same h-9/px-3 frame as the provider search
- Model list flows with the page instead of a max-h capped inner scroller

Other pages:
- Account uses the shared PageFrame/PageHeader: left-aligned, text-3xl
  title, Sign Out in the header actions slot
- Desktop notifications is one General section: header row plus the
  Event/Notify/Sound matrix nested in a card, so its rows no longer read
  as top-level peers of Dark mode; 'Available in the desktop app' label
  removed
- Schedule page retitled from Schedules with a real description; Customize
  description rewritten

Chat composer:
- The voice dictation button only renders once a voice model is
  configured (Settings -> Voice); the unconfigured deep-link state is
  gone (prop type kept for an easy restore)

* chore(desktop): release v0.0.17

* fix(desktop): unblock sdk-test lint on the voice-input model picker (#13553)

The model picker renders a radiogroup of styled buttons with role=radio
and aria-checked; biome's useSemanticElements flags the role as an
error, which fails the sdk-test Quality Checks lint for every PR
touching sdk/ or apps/ paths. Suppress with a justification — switching
to input type=radio needs a restyle and belongs to the desktop settings
work.

* fix(vscode): include rich workspace metadata in system prompt (#13518)

* capture richer workspace information for vs code extension

* fix(shared): redact credentials from workspace remotes

* fix(shared): avoid regex backtracking in remote redaction

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>

* Hide task costs on vscode when ClinePass is selected (#13515)

* fix: stop showing cost estimates for subscription-billed providers (#13552)

* fix(vscode): stop showing cost estimates for subscription-billed providers

Providers whose usage is covered by a flat-rate subscription (ChatGPT
Plus/Pro via openai-codex, ClinePass) are marked with
metadata.usageCostDisplay = "subscription" in the SDK, and the CLI
already suppresses dollar figures for them. The VS Code host collapsed
that value into "show" before it reached the webview, so the task
header and model pricing rows rendered API-rate cost estimates that
users read as real charges on top of their subscription.

Pass all three usageCostDisplay values ("show" | "hide" |
"subscription") through the catalog listing and render cost only when
the value is "show", matching the CLI's shouldShowCliUsageCost
policy.

* feat(llms): mark Claude Code as a subscription-billed provider

Claude Code is typically authenticated with a Claude Pro/Max
subscription, but its models reuse Anthropic API pricing metadata, so
Cline rendered per-token prices and API-rate cost estimates for usage
that is covered by the subscription. Set usageCostDisplay =
"subscription" on the provider (picked up by the CLI and the VS Code
webview) and suppress the price rows in the Claude Code settings card.

The Claude Code CLI can also run on API-key billing, where a real cost
exists; the provider cannot distinguish the two, so we prefer showing
no number over a misleading one.

* fix(vscode): suppress cost display until provider listings load

While the ListProviders request is in flight (or after it fails), the
usage-cost hook had no listing to consult and fell back to "show",
flashing the API-rate estimate at subscription users on every chat-view
mount — the exact display the previous commit removes. Return
"unknown" whenever listings are absent; consumers already render cost
only for "show", so they suppress it during that window with no
changes. Briefly hiding a real cost is harmless, briefly showing a fake
charge is not.

* fix(desktop): reconcile voice settings after main sync

* test(llms): allow experimental ElevenLabs models

* fix(sdk): preserve canonical media model behavior

* feat(desktop): customize macOS DMG install window (#13563)

* feat(desktop): add Retina DMG background tooling

* feat(desktop): customize the macOS DMG layout

* ci(desktop): validate DMG background assets

* fix(desktop): adjust DMG Applications icon position

* ci(desktop): drop redundant DMG artwork validation from publish workflow

Tauri's beforeBuildCommand already runs dmg:background (with its own
validation) at the start of the build/sign/notarize step, and the
release/beta config overlays do not override the build section, so this
step duplicated work the publish job performs anyway. PR-time coverage
lives in desktop-test.yml.

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>

* feat(desktop): sidebar time view, Customize/Marketplace split, and schedule page UX (#13570)

* feat(desktop): split Customize into Installed and Marketplace pages

The Customize hub previously embedded a Browse section inside every tab
that had a catalog. That inlining made each tab long and buried the
catalog. Customize is now the installed inventory only (skills, MCP,
plugins, rules, hooks, tools tabs pass marketplaceVariant="installed"
to the embedded MarketplaceView; McpServersContent grew the same prop),
with an outline Marketplace button in the header.

Browsing moved to a dedicated Marketplace settings section that renders
the previously dead "directory" variant of MarketplaceView: one list
across all catalog types with type-filter chips, wrapping tag chips,
and light rules separating the filter tiers from each other and from
the results. The Clear control now renders inline at the end of the tag
row only while a tag is active, the Updated date is gone, and the
header hosts an Installed button mirroring the one on the Customize
page. Directory subheader copy: "A curated set of plugins, MCP
servers, and skills from the Cline community."

Tag and type chips wrap to new lines instead of scrolling
horizontally.

* feat(desktop): sidebar time view with sections, sort toggle, and scheduled detection

Restores the time-sorted session list as the default sidebar view, with
collapsible Pinned / Scheduled / Tasks sections (headers appear only
once something is pinned or scheduled) and the page-fill effect that
grows the fetched history window until a Show-more click makes visible
progress. Project grouping stays as the alternate mode behind a
one-click sort toggle whose icon reflects the active mode — the old
dropdown cost an extra click for a two-option choice.

Scheduled sessions are detected two ways: the hub-schedule origin
trigger in session metadata, plus a fallback that asks the hub which
session ids belong to schedule executions (list_routine_schedules,
fetched on mount and every two minutes, merged into a rolling set).
The fallback matters because locally executed scheduled runs do not
reliably stamp the trigger into session metadata — a real scheduled
session created today carried only {mode:"user"} provenance. The
scheduled clock icon now leads the row, left of the title; pin and
timestamp stay on the right.

The initial visible page grows from 10 to 30 rows so a tall sidebar
fills instead of stranding a stub of rows over empty space (history
fetches already start at 50).

The expanded sidebar's Customize row now hosts indented Installed and
Marketplace sub-tabs while a customize section is open; the active
sub-tab carries the full selected background while the parent keeps a
subtler one so the two simultaneous highlights read differently.

Also fixes the hover-card flash on click (logo card and session-row
cards): Radix HoverCardContent sits on a DismissableLayer, so a click
on the trigger registers as a pointer-down outside the card and
dismisses it, and the trigger's focus event immediately reopens it.
onPointerDownOutside preventDefault suppresses the dismissal; cards
still close on pointer leave.

* feat(desktop): schedule page row, dialog, and details UX polish

Schedule cards are now click targets: clicking anywhere on a card
outside its controls opens the details dialog (guarded via
closest("button,...") since every inline control, including the Radix
switch, renders a button element), with Enter/Space keyboard support.
The redundant eye button is gone. The remaining edit / run / pause /
delete buttons grow from the 12px icon-sm size to 28px targets with
16px icons, sized consistently with the adjacent enable toggle — the
icons use explicit size-4 classes so the Button base svg rule cannot
shrink them back.

The new/edit dialog gains breathing room between field labels and their
inputs (space-y-2 per field wrapper).

The details dialog no longer scrolls as a whole when the schedule JSON
is long: the dialog is a flex column capped at 85vh, the JSON pre
shrinks to the remaining space (min-h-0) and scrolls internally, and
the Runs tab list scrolls inside the tab the same way.

* feat(desktop): scheduled sessions UX — unified details dialog, run-now handoff, hidden steering, stuck-thinking fix (#13573)

* feat(desktop): merge schedule details into one view and open run-now sessions

The schedule details dialog drops its Overview/Runs tabs: one scrollable
column with the meta grid, the configuration JSON (capped at max-h-64
with internal scroll so it cannot crowd out what follows), and a Runs
section beneath it showing the three most recent runs with a ghost
"Show all N runs" expander (collapsed again whenever a different
schedule's details open). The "Full configuration for this schedule"
subtext is gone; the dialog passes aria-describedby={undefined} so
Radix does not warn about the missing description.

Run now hands you into the session it starts. The trigger command
queues the run and returns before the runner attaches a session id, so
after the toast the handler polls the schedule overview once a second
for up to 15 seconds — which doubles as keeping the page's run status
fresh (refreshSchedules now returns the fetched overview to make that
single-stream) — until the triggered execution reports its session id,
then calls onOpenSession. Guarded so it never auto-navigates after the
user left the page.

* feat(desktop): hide runtime steering messages from transcripts

Scheduled/automation runs inject user-role steering messages each
iteration ("[SYSTEM] This run is not complete until you call
submit_and_exit...", plus a team-obligations variant). The chat view
rendered them as user bubbles, as if the person had typed them — in a
scheduled session the transcript was mostly [SYSTEM] noise.

They are machinery talking to the model, not something the person said
or needs to read, so the transcript now hides them entirely:
MessageBubble renders null for any [SYSTEM]-prefixed user message.
Grouping still treats them as working-row machinery via a single
isSystemSteeringMessage predicate — they collapse into the run's work
span, are never a turn boundary, can never be mistaken for a run's
answer, and never advance the run count even when metadata is missing —
so work-block folding and checkpoint/edit run numbering stay correct.
A finished scheduled session now reads as prompt, work summary, answer.

* fix(desktop): poll history while an attached session's event stream is dead

Opening a scheduled session while (or right after) it runs left the
view stuck on the thinking shimmer until the user switched away and
back. Root cause is in core: the hub daemon executes scheduled runs on
a private LocalRuntimeHost inside createLocalHubScheduleRuntimeHandlers,
while the hub server only projects live events from its own session
host — so session.attach succeeds but no assistant/tool/status events
ever flow. And since multiple hub daemons share cron.db, a run claimed
by a different daemon is invisible to this hub regardless. The proper
core rewiring is tracked as ENG-2474.

Client-side heal that covers every case: while an attached history
session reports a busy status and no chat_event chunk has arrived for
five seconds (and no assistant bubble is mid-stream), poll every three
seconds — re-read canonical history, merged through the same dedupe
path hydration uses, and the session record's status — so the
transcript and the thinking indicator settle in place. Locally driven
turns keep chunks flowing, so the quiet-window guard keeps the fallback
inert there.

* chore(desktop): format workspace selector components

Biome formatting drift that landed on main; picked up by a formatter
pass over components/views/chat.

* fix(desktop): keep stale-stream poll inert during locally driven turns

The fallback poll could fire between a local submit and the model's
first chunk (optimistic user bubble added, stream quiet past the
window, no assistant bubble yet). It then replaced the optimistic
bubble — raw prompt text — with its canonical history twin, which is
stored wrapped in a user_input envelope. The rekey handler that runs
when the stream starts looks for a trailing user bubble matching the
raw prompt, finds only the wrapped copy, and appends a second bubble:
duplicated messages in normal interactive chat.

The poll now stays inert while a local turn is in flight
(turnEpoch !== turnSettledEpoch, or outstanding optimistic user
messages), checked both before polling and again after the snapshot
returns. Hydration marks the turn settled — the mount defaults
(epoch 0, settled -1) otherwise read as an open turn and would keep
the fallback inert forever for the scheduled-session case it exists
for. Applying a polled snapshot also rebuilds the live tool routing
keys, same as hydration, so later tool events update canonical rows
in place instead of appending.

* fix(desktop): keep the working indicator alive for narrating scheduled runs

Watching a scheduled run live: the first tool row appeared, then the
thinking indicator vanished with nothing streaming, and the rest of
the run (final answer, submit_and_exit) only showed up seconds later
in one lump.

inferHydratedChatStatus treats a "running" session record whose
transcript ends on an assistant message as a session that died without
a status flip and reports "completed". That heuristic is right for
stale records, but scheduled/automation models narrate between tool
calls, so a polled snapshot can genuinely end on assistant text
mid-run — the completed flip hid the working indicator, folded the
run early, and disarmed the stale-stream poll (status left the busy
set), dead-ending live updates until an in-flight poll happened to
deliver the finished run.

The heuristic now only applies once the transcript has actually gone
quiet (newest message older than two minutes — comfortably past model
latency plus tool runs). A recently active transcript keeps the
record's "running" verdict, so the indicator stays up and polling
stays armed until the record itself settles.

* fix(desktop): stale-stream poll mirrors the session record instead of inferring

Replaces the previous fix for the vanishing working indicator (the
time-window guard added to inferHydratedChatStatus) with a version
that adds no inference at all: the heuristic is restored to exactly
its long-standing form, and the poll now maps the session record's
status verbatim (mapSessionRecordStatus).

The record is the right authority in the poll's context: the sessions
this fallback serves have a live host maintaining their record, and it
flips to a terminal status when the run ends. Transcript-shape
inference belongs only where it has always lived — hydrating sessions
whose records may be orphaned — and would misread a mid-run snapshot
ending on assistant narration as a finished session, hiding the
working indicator and disarming the poll.

* fix(desktop): address review findings on steering detection and run-now matching

Steering detection additionally requires the injected-message marker
(meta.userRunSpan === 0) beside the [SYSTEM] prefix, so a person's
genuine prompt that happens to start with "[SYSTEM]" stays visible
and turn-counted. The failure direction is deliberate: an unstamped
injected reminder would merely show as a user bubble, while the
content-only check could hide a real prompt.

Run-now only follows the execution id the trigger reply itself named;
the newest-execution-for-this-schedule fallback could open a previous
run's session when the trigger failed to enqueue one.

* fix(desktop): report a failed run-now instead of confirming a start

A trigger reply without an execution means no run was enqueued (the
schedule may have been disabled or deleted since the page loaded). The
handler previously toasted "Run started" regardless and then silently
skipped the session-open polling. It now shows a destructive
"Run not started" toast, refreshes the schedule list so the row
reflects reality, and skips the polling entirely.

* fix(desktop): don't block the main thread on quit while stopping the sidecar (#13566)

Quitting the mac app beach-balled for ~5-7s. The shutdown POST was
built from the ws transport URL (appending /shutdown lands inside the
query string), so the sidecar was never told to exit, and stop() then
polled the child for up to 7s on the main thread - on macOS inside
applicationWillTerminate - before SIGKILLing it.

stop() now sends SIGTERM and returns immediately. The sidecar handles
SIGTERM with the same bounded (5s) graceful shutdown as the /shutdown
endpoint and exits itself, finishing session persistence as an orphan.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): hover trash button on sidebar session rows (#13582)

Each session row shows a trash icon on the right while hovered (or
when the button itself is focused), opening the same delete
confirmation dialog the row's context menu uses. The row is a button
and buttons cannot nest, so the trash is an absolutely positioned
sibling inside a group/row wrapper, overlaid where the timestamp sits:
row hover hides the timestamp, shows the trash, and moves the row's
hover background to the wrapper group so it holds while the pointer is
on the trash itself.

* fix(desktop): install marketplace plugins and MCP servers in-process instead of spawning a cline binary (#13585)

* fix: install marketplace plugins and MCP servers in-process instead of spawning a cline binary

The desktop app sidecar and cline-hub shelled out to 'cline plugin install'
and 'cline mcp install' for marketplace installs. Packaged GUI apps inherit
launchd's minimal PATH on macOS and most desktop users have no cline CLI
installed at all, so installs failed with a red
'Executable not found in $PATH: "cline"' error.

Install via @cline/core's installPlugin/installMcpServer in-process instead,
matching what the VS Code extension already does. Also fix
parseMcpInstallArgs in @cline/core to treat the marketplace catalog's '--'
separator as end-of-options; previously the separator itself became the
stdio command.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop test-injection plumbing from marketplace installers

Call @cline/core's installPlugin directly instead of threading an
installer option through the marketplace entry points; tests stub the
core module instead.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert: keep cline-hub marketplace installs CLI-backed

The hub dashboard is launched via 'cline dashboard', so a CLI is always
present and CLINE_WRAPPER_PATH resolves it; the PATH bug only affects
the desktop app, which does not ship a CLI.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.18

* chore(vscode): release v4.1.16

* chore(sdk): release v0.0.80

* chore(cli): release v3.0.59

* fix(hub): stop shipping full transcripts inside broadcast hub events (#13587)

* fix(hub): stop shipping full transcripts inside broadcast hub events

Every session.updated (and session.created/detached/run.started) event
embedded the session's ENTIRE message transcript via readCoreSessionSnapshot,
even though no consumer reads snapshot.messages off an event — clients fetch
messages with the session.messages command. For a multi-megabyte transcript
this turns every status flip into megabytes per subscriber, floods the
durable event log, and (until the send-queue backpressure fix lands) lets a
slow subscriber balloon the hub process by one full transcript copy per
event — reported as a 25GB cline process on a 16GB Mac.

Strip snapshot.messages centrally in HubServerTransport.publish() so every
current and future event publisher is covered, the event log stores slim
envelopes, and cursor replay stays byte-identical with live fan-out. All
other snapshot fields (status, usage, model, workspace, checkpoint) are kept,
and command replies are untouched.

* fix(hub): never capture the transcript into event/reply snapshots

Replaces the publish-boundary strip with the real fix: don't build
message-bearing snapshots in the first place. emitSessionSnapshot no longer
re-reads the entire transcript from disk on every status flip, and
readCoreSessionSnapshot no longer reads it for any event or reply — a
snapshot is a state notification (status, usage, model, workspace,
checkpoint); the transcript is fetched via the session.messages command.
Checkpoint-restore snapshots (session-versioning-service) are untouched:
restore replies carry messages in their own dedicated field.

* chore(desktop): release v0.0.19

* chore(sdk): release v0.0.81

* chore(cli): release v3.0.60

* fix(vscode): avoid render crash on malformed api_req payloads in combineApiRequests (#13560)

* fix(vscode): stop pinning DeepSeek model count in catalog smoke test (#13600)

* feat(ui): share agent welcome hero (#13567)

* feat(ui): share agent welcome hero

* test(ui): cover welcome hero pointer states

* refactor(ui): keep welcome hero API minimal

* test(ui): verify welcome hero package assets

* fix(ui): inline welcome hero masks

* fix(tools): preserve a file's own CRLF line endings across apply_patch updates (#13512)

* fix(desktop): keep the window title bar draggable across views (#13572)

* fix(desktop): keep window title bar persistent

* fix(desktop): reserve persistent title bar space

* fix(desktop): polish persistent title bar layout

* Sign Windows CLI binaries with Azure Trusted Signing; surface app-control launch errors (#13021)

* feat(cli): sign Windows binaries with Azure Trusted Signing and surface app-control launch errors

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): use _CLI-suffixed signing profile secret, normalize endpoint, fail loud on partial config

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* Tunnel ProtoBus over the existing Host Bridge (#13218)

* feat(core): tunnel ProtoBus over Host Bridge

* fix(core): harden Host Bridge stream lifecycle

* fix(core): serialize concurrent chunked responses per request

Streaming handlers deliver updates fire-and-forget, so two logical
responses for one request_id can be in flight at once. Chunked payloads
made forwarding non-atomic: each chunk write is an await, so concurrent
forwards could interleave their chunk sequences and the receiver --
which reassembles purely by arrival order -- would splice two payloads
into one. Route all forwards for a request through one promise chain; a
failed write rejects every later forward so a torn payload is never
followed by more chunks.

Rename the lock manager's instanceAddress to instanceOwner: it holds an
opaque per-spawn instance ID on the token path and a listener address
only on the CLI-harness path. Delete the caller-less getInstanceByPort
query that interpreted the owner as an address.

Also: document message_json as a legal wire encoding for small
payloads, close the gRPC client when startup fails, note the
intentional discard of the cancellation confirmation, and add the
proto's trailing newline.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>

* Build and Authenticode-sign a Windows x64 desktop installer in desktop releases (#13607)

* feat(desktop): build and Authenticode-sign a Windows x64 NSIS installer in desktop releases

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): pin OIDC-adjacent actions to commit SHAs in the Windows signing job

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): pin checkout and upload-artifact to commit SHAs in the Windows signing job

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): show agent-created schedules on the Schedules page (#13613)

* fix(desktop): show agent-created schedules on the Schedules page

Schedule hub commands are scoped to the workspace registered by the
connection, but the desktop app's hub client registers the app launch
directory while agent-created schedules live under each chat's own
workspace folder - so they never appeared on the Schedules page.

Grant token-authenticated hub connections (which can already bind any
workspace at registration) explicit cross-workspace schedule access via
an allWorkspaces payload flag, and have the desktop sidecar request it
for routine schedule commands. Workspace-bound clients (local browser
origins) and default CLI behavior stay scoped.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(core): strip allWorkspaces flag from schedule inputs and pin it in the sidecar payload

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make suggested routine template prompts prescriptive about their final output (#13611)

* Make bug hunter routine template prescriptive about its final report

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make remaining routine templates prescriptive about their final output

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop: surface scheduled-task final output — auto-expand submit_and_exit and render its summary as markdown (#13612)

* desktop: auto-expand submit_and_exit and render its summary as markdown

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: render submit summary in full foreground color

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: label the submit row 'Scheduled task completed'

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: label errored submit_and_exit rows as failed

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add tooltips explaining Live and After recording badges on voice input models (#13610)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Remove box shadow from chat message actions row (#13630)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): make the Tauri shell work on Windows (#13632)

- Defer updater installation to the user-initiated restart on Windows:
  install() launches the NSIS installer and exits the process immediately,
  so the background cycle now downloads only and stages the bytes, and
  restart_to_apply_update installs them after stopping the sidecar.
- Spawn child processes (sidecar, git, cmd /C start) with CREATE_NO_WINDOW
  so the GUI-subsystem app doesn't pop visible console windows.
- Fall back to USERPROFILE when HOME is unset resolving the MCP settings
  path, matching the sidecar's homedir().
- Reap the sidecar after the Windows hard-kill so its exe file lock is
  released before the NSIS installer replaces it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop watching agenda spec dirs while the todo tool is disabled (#13629)

* fix(core): stop watching agenda spec dirs while the todo tool is disabled

Since #13530 disabled the agent todo tool, the Agenda UI, and the
automation pump, the hub still created fs.watch watchers on the global
agenda specs dir and on every workspace root recorded in the task store
(at startup and on scope access). Nothing consumes the watcher-driven
task events while the feature is off, and the task.* hub commands
already reconcile spec files on demand, so the watchers are pure
overhead - one OS watch handle per known workspace.

Wire watchFiles to AGENDA_TODO_TOOL_ENABLED the same way
automationEnabled is, preserving a host's explicit watchFiles opt-out
for when the flag is turned back on. Schedules are unaffected: the
schedule list has no file watcher and updates through hub commands and
published schedule events.

* fix(core): reconcile external spec edits inside updateTask

With the spec watchers off there is no background reconciliation, so a
task spec edited directly on disk made every same-store task.update fail
the signature check with "task spec changed outside the manager" until
an unrelated task.get or task.list happened to reconcile the scope.

Reconcile the task's scope at the start of updateTask (mirroring what
refreshAndVerifyTaskIntent already does for approve/run), skipping it
when the file reconciler itself is the caller to avoid recursing from
reconcileFileStore. An external edit now surfaces as the store's normal
stale-revision conflict, and a re-read-and-retry succeeds. This also
closes the pre-existing watcher debounce race for updates.

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently (#13565)

* fix(sdk): don't log out Codex/OCA users when token refresh fails transiently

Port the cline-provider refresh semantics to openai-codex and oca:
a transient refresh failure (network error, timeout, server 5xx) with an
already-expired access token now rethrows instead of returning null.
A null return means the refresh token was REJECTED and re-auth is
required; treating an outage blip as a rejection is what turned it into
a forced 'openai-codex requires re-authentication.' task stop while the
settings UI still showed the user as signed in.

Both providers also emit user.auth_refresh_soft_failure telemetry on
transient failures (the 'prevented logout' counter the cline provider
already has) and attach status/errorCode details to the genuine
invalid_grant logout event.

* refactor: collapse duplicate soft-failure telemetry branches and test

Review feedback: compute tokenExpired once and emit the soft-failure
event once in both providers, then return current credentials or
rethrow. Fold the codex soft-failure telemetry assertions into the
existing still-usable-token test instead of a near-duplicate case.

* fix: make OpenAI Codex (ChatGPT subscription) sign-in fail loudly instead of silently dead-ending (#13537)

* fix: make OpenAI Codex sign-in fail loudly instead of silently dead-ending

When callback port 1455 is already in use (e.g. by the Codex CLI or a
previous pending sign-in), startLocalOAuthServer returns a no-op server
and loginOpenAICodex would open the browser anyway, then dead-end:
the callback could never be received, and in the VS Code extension the
user just saw nothing happen after clicking 'Sign in to OpenAI Codex'.

- loginOpenAICodex now fails fast with an actionable 'port in use'
  error before opening the browser, unless the host provides manual
  code entry (the CLI's paste fallback keeps working)
- surface OAuth redirect errors (e.g. access_denied) instead of
  collapsing them into 'Missing authorization code'
- the extension dedupes concurrent sign-in clicks: a re-click re-opens
  the auth page of the pending flow instead of spawning a second flow
  that would collide with our own callback server
- browser-open failures now show an error message with the URL to
  open manually instead of only logging
- abandoned-flow timeouts no longer surface a confusing 'Missing
  authorization code' toast

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop host-side codex login dedupe, keep flow identical to CLI

The SDK owns the failure handling now (fail-fast on an unbindable
callback port), so the extension keeps the exact same simple
loginOpenAICodex call the CLI uses. A second click while a flow is
pending gets the SDK's clear port-in-use error, same as running
'cline auth openai-codex' twice would. Keep only the CLI-parallel
onOpenUrlError surfacing (the CLI prints 'open the URL above
manually'; the extension's equivalent is an error toast with the
URL).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(e2e): cover Codex sign-in callback-port failure and redirect errors

Two driven-VS Code tests for the OpenAI Codex (ChatGPT subscription)
sign-in flow:

- with port 1455 occupied on both loopback families, clicking the
  sign-in button surfaces the fail-fast port-in-use toast
- with the port free, the callback server binds and an OAuth redirect
  error (access_denied) propagates to a visible error toast

The second test opens a real browser tab to the OpenAI auth page as a
side effect of the genuine sign-in click.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* feat(core): anchor agent-created schedules in the user's .cline schedules home (#13634)

* feat(core): anchor agent-created schedules in the user's .cline schedules home

Agent-created schedules inherited whichever workspace folder the chat
session happened to run in, scattering user-level routines across chat
and project folders. They were invisible to workspace-scoped listings
elsewhere, tied to folders that may be cleaned up, and each chat's
tasks tool saw a different set when checking for duplicates.

Anchor them in ~/.cline/schedules instead: the hub's scheduled-task
session defaults now resolve to that home (created on demand), so
agent-created schedules live and run in one stable user-level scope.
The tasks tool guidance now tells agents that scheduled sessions run in
the schedules home, so prompts must carry absolute paths to any project
they operate on.

Schedules created explicitly with a workspace (CLI --workspace, desktop
routine wizard) are unchanged, and existing rows keep their current
workspaceRoot - they stay visible through the all-workspaces listing
paths (#13613, #13633).

* test(core): restore any pre-existing CLINE_DIR after the agenda hub test

The test's cleanup deleted CLINE_DIR outright, so an environment that
had it configured would leave later tests in the same worker on the
default storage directory. Save the previous value and restore it.

* test(core): restore CLINE_DIR even when hub test setup throws early

Restoring the override in the try/finally missed failures thrown during
transport construction or start(), before the try was entered. Register
the restore with onTestFinished instead, which runs regardless of where
the test fails.

* fix(desktop): don't show providers as configured without real credentials (#13608)

* fix(desktop): don't show providers as configured without real credentials

The desktop settings marked any provider with a persisted settings entry
as Configured, but legacy VS Code migration and empty saves can seed
entries (e.g. qwen-code, sapaicore) holding only a default model and no
credentials. Move the CLI's isProviderSettingsUsable readiness check into
@cline/core, expose it as a computed 'configured' flag on the provider
catalog, and use it in the desktop's isProviderConnected.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): resync catalog after saves so Configured badge updates live

Optimistic provider mutations can't know the sidecar-computed 'configured'
flag, so after connecting a keyless provider or saving cloud credentials
(e.g. a Vertex project id) the row stayed 'Not configured' until remount.
Silently refetch the catalog after each successful save, guarded by the
existing generation counter so newer edits discard stale responses.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): claim a generation in post-save resync so overlapping refreshes can't apply stale snapshots

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): bump catalog generation on OAuth login success

Every other optimistic provider mutation claims a new generation; the
OAuth success path didn't, so a catalog load or resync still in flight
could arrive late and overwrite the just-connected state.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): resync catalog after OAuth login instead of bare generation bump

The resync claims a new generation (discarding any stale in-flight
response) and its own fetch covers both the new OAuth connection and any
provider saved moments earlier, matching the post-save path.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint (#13626)

* fix(core): refuse checkpoint workspace restore when HEAD moved past the checkpoint

Restoring a checkpoint runs git reset --hard, which moves the current
branch pointer. If commits were made after the checkpoint (by the user
or by the agent), the reset silently knocked them off the branch,
leaving them reachable only through the reflog.

Guard the reset: if HEAD no longer matches the commit the checkpoint
was created on, throw a descriptive error (including how many commits
would be dropped) instead of destroying history. Chat-only restore is
unaffected, and users who really want to discard the commits can reset
the branch manually first.

Fixes #13550

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): close the guard-to-reset race with an atomic ref update

The moved-HEAD guard read HEAD, ran further git commands, then reset
unconditionally, so a commit landing in that window could still be
knocked off the branch. Replace the reset's branch move with git's
native compare-and-swap (git update-ref HEAD <new> <old>), which fails
if HEAD no longer points at the verified commit, and follow with a bare
reset --hard to sync the index and worktree to the already-moved HEAD.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: hide history cost estimates for subscription-billed tasks (#13562)

* fix(vscode): hide history cost estimates for subscription-billed tasks

The task-header fix for subscription providers cannot reach history:
history rows render the stored totalCost (an API-rate estimate) and do
not know which provider ran the task, so the history page printed
$X.XXXX on every row and the recent-task chips in an empty chat view
rendered a $ chip even for subscription-billed tasks.

The SDK session records already persist the provider — the CLI's
history view uses it for exactly this — but the VS Code mappers dropped
it. Map it through both transports (HistoryItem.apiProvider for the
state-pushed taskHistory, TaskItem.api_provider for getTaskHistory) and
suppress the dollar figure per row when that provider's
usageCostDisplay is not "show", via a new useUsageCostVisibility
predicate shared by both surfaces.

Rows without a recorded provider (tasks predating the field, legacy
imports) keep showing the stored value — there is nothing to key
suppression on.

* test(vscode): e2e-verify history cost suppression in real VS Code

Seeds SDK session records (one openai-codex subscription task, one
anthropic usage-billed task) into the isolated CLINE_DIR before the
webview loads, then asserts in a real VS Code instance that both the
recent-task chips and the full history page render the dollar figure
only for the usage-billed task. Covers the two boundaries the unit
tests stub: on-disk records reaching getTaskHistory with provider
populated, and the provider listings delivering the subscription mark
to the webview.

* Fix scheduled tasks disappearing after desktop app updates (#13627)

* Fix hub-managed schedules being wiped by cron reconciliation on hub restart

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Require the virtual hub/schedules path when exempting specs from removal reconciliation

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Treat recorded source mtime as proof a spec is file-backed, closing the hub/schedules spoof gap

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(shared): discover global rules at ~/Cline/Rules (#13614)

The VS Code Rules tab resolves the Documents folder via
'xdg-user-dir DOCUMENTS', which prints bare $HOME when no user-dirs
config exists (WSL/headless installs), so it reads and writes global
rules at ~/Cline/Rules. The SDK's rule search paths only covered
~/Documents/Cline/Rules, so those rules never reached the system prompt.
Add the missing path to the search list.

Fixes #13542

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat: add searchable session history (#13420)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* Fix CLI crash when a remote MCP server is offline but enabled (#13639)

Remote (SSE/streamable HTTP) MCP connects run on the session.create
critical path, which the hub caps at 30s. Without a connect budget an
unreachable server spent the full 60s default request timeout (with the
SSE transport stuck in a reconnect loop), stalling session.create past
the hub deadline and tearing the whole session down - the interactive
TUI exited and one-shot runs failed. Stdio servers already have a
bounded initialize budget for exactly this reason; give URL clients the
same treatment with a 10s default connect budget that an explicit
timeout overrides in either direction.

Fixes #13597

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(vscode): prevent E2E worker teardown hangs (#13644)

* test(vscode): capture external URLs in E2E runs

* docs(test): clarify browser capture rationale

* Add a GitHub integration step to the onboarding (#13225)

* Add feature flags to the app

* React to account updates

* Address comments

* Add a GitHub integration step to the onboarding

* validate domain and fix errors on auth

* Hide the step behind a feature flag

* update version

---------

Co-authored-by: John Choi <john.choi@cline.bot>

* fix(ci): stop e2e worker teardown timeouts and deflake hub daemon e2e on Windows (#13646)

* fix(e2e): stop VS Code e2e worker teardown from timing out

The ext-vscode-test-e2e job has been failing on main with 'Worker teardown
timeout of 60000ms exceeded' even though every test passes. Playwright only
reports an Electron app as closed once the process exits AND every holder of
its stdio pipes is gone (ChildProcess 'close' waits on the extra fd3/fd4
pipes Playwright creates for Electron). Any VS Code descendant that outlives
the main process (chrome_crashpad_handler, GLib's 'dconf watch' helper,
xdg-open browser handlers, VS Code 1.135's agent host CLI subprocess that
logs 'unable to kill the process') keeps those pipes open, so app.close()
never resolves and the worker teardown hangs on it until its 60s timeout
fails the job.

Harness fixes, each removing one source of that wedge:

- closeAppForTeardown now SIGKILLs the whole process group (taskkill /T on
  Windows) when app.close() times out, instead of only the main pid — and
  does so even when the main process already exited, which is exactly the
  wedged state. Playwright launches Electron detached, so pid == pgid.
- Launch VS Code with --disable-crash-reporter so no crashpad handler
  outlives the app holding the harness pipes.
- Seed the fresh user-data-dir with chat.disableAIFeatures: true so VS
  Code's own AI features (rolled out via server-side experiments, so CI
  breaks without any repo change) never start their agent host process.
- Drop the page.close() teardown: closing VS Code's last window quits the
  whole app, and ElectronApplication.close() on an already-exited app
  deadlocks; the app fixture's app.close() closes windows itself while the
  app is alive.
- Codex sign-in no longer opens a real external browser under E2E_TEST; the
  codex-oauth test drives the OAuth callback itself, and the browser was an
  orphaned process holding the harness pipes on the runner.

* fix(core): deflake hub daemon e2e tests on Windows runners

sdk-test on windows-latest fails intermittently in the hub daemon e2e
files:

- shutdown.e2e.test.ts dies with a bare 'Error: socket hang up'. That
  message is the ws handshake (http.ClientRequest) failing, not the
  /shutdown fetch (an undici failure prints 'TypeError: fetch failed'):
  a freshly spawned bun daemon on a loaded 2-core Windows runner
  occasionally drops its first accepted connection before writing the
  upgrade response. Real hub clients reconnect with backoff, and the test
  asserts shutdown behavior rather than first-connection reliability, so
  openAuthenticatedSocket now retries transient handshake failures within
  a 15s budget.
- singleton.e2e.test.ts times out waiting for daemon discovery: it still
  used the 10s hang guard that 0cfc90158 already raised to 30s in
  shutdown.e2e.test.ts for the same reason. Use the same 30s guard.
- Raise the e2e testTimeout to 60s so a test that legitimately spawns two
  daemons back to back can survive slow-runner startups instead of the
  discovery hang guard being cut off by the test timeout.

* feat(desktop): render tool output images as attachments (#13643)

* fix(desktop): render tool output images as attachments

Add support for displaying media returned by tool calls (e.g. screenshots)
as rendered images with expand-to-fullscreen capability instead of raw
base64 text. Introduces an `ImageCarousel` component for navigating
multiple images, propagates the expand handler to tool message blocks,
and extracts/validates output media in tool summaries.

* test: cover multi-image and canonical media extraction in tool output (#13645)

extractOutputMedia and the desktop tool-message rendering path were only
ever exercised with exactly one distinct valid image, and
canonicalInlineMedia (MCP-style type: "media" blocks for audio/video/file)
had zero coverage. Add tests for: multiple distinct images in one tool
result (parser + desktop carousel navigation), inline audio via the
mime_type key spelling, canonical video/file media blocks, and rejection
of an invalid canonical image block.

---------

Co-authored-by: Harrison <harrison@cline.bot>

* chore(desktop): release v0.0.20

* feat(sdk): add discovery boundary ahead of Agent Plugins support (#13017)

* ENG-2490: Propagate session aborts to teammates (#13647)

* fix(core): propagate session abort to teammates

* fix(core): persist aborted teammate tasks as cancelled

* fix(core): settle teammate work on session abort

* fix(core): isolate replacement runs from stale aborts

* refactor(core): narrow teammate task status metadata

---------

Co-authored-by: abeatrix <beatrix@cline.bot>

* fix(llms): use AI SDK 7 Langfuse telemetry (#13651)

* fix(llms): use AI SDK 7 Langfuse telemetry

* test(llms): cover Langfuse runtime context

* chore(llms): built-in model list update 1787907289186 (#13663)

* chore(llms): built-in model list update 1787907289186

Result of `bun run build:models`.
Includes updated model list and fixed formatting issues across codebase.

* test(llms): update GLM reasoning toggle expectation

* test: cover session search fallback on hub timeout and rejection (#13642)

* feat: add searchable session history

Rebased onto main and updated to supersede the sidebar search dialog
from #13533: the sidebar search icon now opens the indexed command bar
(Cmd/Ctrl+P) instead of a sidebar-local cmdk dialog that eagerly loaded
the entire session history via loadAllSessions(). CommandDialog gains a
shouldFilter passthrough so server-ranked FTS hits are displayed as-is.

* fix: harden session history search

* fix: evict failed restoration sessions from search

* fix: preserve deletion when search eviction fails

* fix: address session search review feedback

* fix: preserve search suppression during reconciliation

* test: cover sidecar search fallback on hub timeout and rejection

The existing search_sessions tests only exercised the index-hit and
empty-index-fallback paths with an immediately-resolved hub reply.
Add coverage for the two other realistic Hub-connection failure
modes the fallback is meant to tolerate: the hub call rejecting, and
the hub call hanging past the 750ms withSearchDeadline race.

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* fix(core): refresh Cline models from live catalog (#13670)

* feat(ui): share attachment drop zone (#13672)

* feat(ui): share attachment drop zone

* fix(ui): cancel disabled attachment drops

* chore(ui): simplify drop zone surface

* chore(ui): release v0.2.0-next.8

* Chore/bump undici mermaid (#13675)

* chore(deps): bump mermaid to 11.16.1 and raise undici floor to 7.29.0

* chore(deps): patch js-yaml and body-parser in the npm-managed subprojects

* fix(llms): make Langfuse tracer detection survive minified release builds (#13680)

* fix(llms): recognize direct tracer providers

* fix(llms): make Langfuse tracer detection survive minified release builds

Release binaries are compiled with minify enabled, which renames classes,
so initializeLangfuseTelemetry's constructor-name guard never matched
"ProxyTracerProvider" and silently returned readiness=false in every
production build (hub log: "creating span processor" followed by
"initialized readiness=false" with no branch message in between). Dev runs
execute unminified source, which is why the same env vars worked there.

Replace every constructor-name comparison with checks that survive
minification: detect the proxy structurally via getDelegate, distinguish a
recording provider from the no-op fallback by its lifecycle methods, and
confirm our NodeTracerProvider registration by object identity. When a
foreign provider already owns the global slot, attach the Langfuse span
processor to it when it accepts processors, and otherwise shut down the
orphaned provider and report the rejection instead of bailing silently.

Verified by bundling the module with Bun minify:true against the real
OpenTelemetry packages: the previous code reproduces readiness=false
(provider class name mangles to "H2"), the new code initializes with
readiness=true.

* fix(vscode): prevent hook spawn failures from crashing the core process (#13422)

* fix(vscode): prevent hook spawn failures from crashing the core process

A hook child-process spawn failure emitted "error" on HookProcess with no
listener registered, which Node's EventEmitter turns into an uncaught
exception - killing the entire cline-core process instead of failing the
one hook open. Guard the emit behind listenerCount so the rejection (which
StdioHookRunner handles) is the only propagation path.

The trigger was a workspace root that no longer exists on disk passed as
the spawn cwd: Node reports a nonexistent cwd as a misleading ENOENT on
the launcher binary ("spawn /bin/sh ENOENT"). Validate cwd existence in
HookProcess right before spawning - falling back to no explicit cwd with
a warning that names the missing directory - and when a spawn still fails
ENOENT because the directory vanished in between, name it in the error
message instead of blaming the shell.

* fix(vscode): fail hooks with a missing working directory instead of relocating them

Running a hook whose assigned cwd no longer exists from the host
process's own working directory would let its relative paths read and
write an unrelated location (e.g. the IDE install directory). Reject
before spawning, with an error naming the missing directory; the runner
reports the hook as failed and the task continues. Also carry pre-spawn
failure messages into HookExecutionError details so the cause is not
reduced to a bare "exited with code 1".

* fix(vscode): thread task id into hook runner creation so execution telemetry fires (#13547)

The SDK hooks adapter created every hook runner without a task id, and
StdioHookRunner gates all captureHookExecution calls on one being set —
so the next variant emitted zero hooks.execution events while discovery
telemetry fired normally. Pass the task id (and tool name for the tool
hooks) at all five factory.create call sites, and pin the threading
with a regression test.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: JasmineLCY <38378321+JasmineLCY@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Haley Park <haleypark.design@gmail.com>
Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>
Co-authored-by: yzxcj797 <54314860+yzxcj797@users.noreply.github.com>
Co-authored-by: yzxcj797 <yzxcj797@users.noreply.github.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: 𝓜𝓲𝓼𝓼𝓪𝓻𝓲 𝓐𝓱𝓲𝓵 🌿 <143264692+missarii@users.noreply.github.com>
Co-authored-by: Dominic Cooney <dominic.cooney@cline.bot>
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Harrison <harrison@cline.bot>
Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: TheRealSpencer <32678829+TheRealSpencer@users.noreply.github.com>
2026-08-31 11:57:01 -07:00
Bee 7844ae9250 feat(desktop): Composio connectors on desktop-experimental (#13685)
* feat(desktop): Composio connectors (Gmail, Google Calendar, GitHub + catalog)

Port of the connectors feature from main (bee/poc-connectors-composio,
PR #13684) onto desktop-experimental, adapted to this branch's inline
marketplace: with no separate Marketplace page here, the Customize >
Connectors tab hosts both the installed view (API key management,
recommended Gmail / Google Calendar / GitHub, connected accounts) and
the browsable connectable-only catalog below it. The Marketplace-page
filter-chip wiring is kept dormant for a cleaner future merge with main.

Feature summary:
- Sidecar management plane (sidecar/composio.ts): key handling with a
  COMPOSIO_API_KEY env fallback, OAuth connect (authorize with a
  connected-accounts link fallback, custom auth configs preferred),
  dashboard reconciliation, and a usage-ranked catalog filtered to
  toolkits with Composio-managed credentials or a project auth config.
- Connected toolkits materialize as a generated single-file plugin in
  ~/.cline/plugins that the Hub loads into new sessions; tools execute
  against Composio's REST API with versions pinned at connect time.
- Rows use Install / View; Uninstall lives in the fixed-size detail
  dialog; official catalog logos with themed fallbacks.

* fix(desktop): guard stale OAuth finalization and surface plugin sync failures

Review follow-ups on the Composio connectors:

- finalizeToolkitConnection now takes a guard (attempt id + the key/user
  the attempt started under) and re-checks it synchronously at write
  time, after the awaited tool fetch — a cancel, disconnect, or key
  change landing mid-flow drops the stale result instead of resurrecting
  a disconnected account or binding an old-project account to a new key.
  setComposioApiKey cancels in-flight attempts when the key changes, and
  the dashboard reconcile re-reads state before writing and skips
  toolkits disconnected after its remote snapshot was taken.
- syncComposioPluginFile now throws on filesystem failure. User-initiated
  paths surface it: connect keeps the recorded connection but reports
  that new sessions will not see the tools; disconnect and key changes
  fail loudly when the plugin file could not be updated. Passive status
  reads keep a log-and-continue wrapper.
2026-08-30 11:23:24 -07:00
John Choi 6540642942 Gate GitHub onboarding rollout on desktop experimental (#13624)
* feat(desktop): gate GitHub onboarding rollout

* fix(desktop): expose rollout flags with cloud gate
2026-08-27 17:29:19 -07:00
+8 e04e23c52d chore(desktop): sync latest main into desktop experimental (#13523)
* fix(vscode): continue the surviving session on resume instead of rebuilding with the original task text (#13175)

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilt the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

The preserved conversation history is the source of truth on resume, so
the fallback prompt now just asks the model to reassess the history and
continue, matching the legacy resume prompt which also never resent the
original task. User-typed text still takes precedence when provided.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): continue the surviving idle session on follow-ups instead of rebuilding

Stopping a turn keeps the session alive, but every idle follow-up (bare
Resume after Stop, and typed follow-ups after a completed turn) tore
that session down and rebuilt it from persisted task history before
sending. Continue the matching idle session in place instead, the same
way the CLI reuses the live session after an abort. Rebuilding from
history now only happens when no live session matches the displayed
task (task opened from history, extension host reload).

A bare resume still needs a prompt to start a turn, so it sends the
neutral [TASK RESUMPTION] prompt (shared with the rebuild fallback and
hidden from the transcript); user-typed content is echoed and sent
as-is. If the send lands while the abort is still settling, the runtime
auto-queues it and drains once the abort completes.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): consolidate follow-up send paths in SdkFollowupCoordinator

Now that idle follow-ups continue the live session in place, the
two-mode sendToActiveSession helper was redundant: its non-queued branch
duplicated continueIdleSession minus the bare-resume prompt. Split it
into a single-purpose queueToActiveSession and fold the idle no-task
send into continueIdleSession, flattening askResponse's decision tree
to: queue onto a running turn, continue a matching live idle session,
rebuild from history, or abandon.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): reuse the existing neutral resumption prompt for bare resumes

Drop the newly invented long resumption wording in favor of the phrase
that already existed as the no-history fallback and that the transcript
hiding logic and test fixtures recognize: '[TASK RESUMPTION] Please
continue where you left off.' The net change to resumeSessionFromTask
against main is now just deleting the branch that resubmitted
historyItem.task as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilds the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

Bare resumes now always use the neutral prompt that already existed as
the no-history fallback; user-typed text still takes precedence. This
matches the legacy resume prompt (responses.taskResumption), which only
ever included user-supplied text as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): hide synthetic prompts from the queued-prompt echo

A send that races a settling abort is auto-queued by the runtime, so a
bare Resume can reach the pending_prompt_submitted echo carrying the
synthetic [TASK RESUMPTION] prompt. Echoing it leaked model-facing text
as a visible user bubble and shifted the visible-user-message ordinals
that edit/regenerate mapping relies on. Filter synthetic prompts with
isSyntheticUserPrompt, keeping user attachments visible (matching
isSyntheticSdkUserMessage semantics).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): preserve LiteLLM input token limits (#13293)

* fix(vscode): preserve LiteLLM input token limits

* fix(vscode): prefer live LiteLLM model metadata

* fix(vscode): generalize private catalog metadata

* test(vscode): preserve llms exports in vscode lm mock

* fix(vscode): point provider signup URLs at their API key pages (#13337)

* fix(vscode): point Mistral signup URL at the general API keys console

The Mistral provider's signup link led to the Codestral console, which
issues Codestral-scoped keys that fail with 401 on api.mistral.ai — the
endpoint the provider actually calls. Point it at the general API keys
page instead.

Fixes #13288

* fix(vscode): deep-link DeepSeek and Fireworks signup URLs to their API key pages

Both pointed at marketing homepages; link straight to the key-creation
pages instead, matching the rest of the registry and the desktop app's
provider-key-urls map.

* fix(ci): always build the legacy bundle from the legacy-extension branch (#13349)

The combined-VSIX workflow took legacy-ref as a free-form dispatch input
with no publish-time validation (next-ref has one: publish requires main).
Any typed ref — a PR merge ref, an unprotected branch — would be built
into the published VSIX by the environment-less build job, and the publish
environment approver only ever sees an opaque prebuilt artifact, so the
approval protected the marketplace PAT but not the shipped bytes.

Remove the input entirely and hardcode the protected legacy-extension
branch, which makes that branch's protection rules load-bearing for
releases. The tested-sha pinning between test-legacy and build is
unchanged. publish-extension skill dispatch command updated to match.

* fix(ci): lock the legacy publish workflow to the legacy-extension branch (#13350)

The branch dispatch input was a free-form string with no validation. Both
jobs checked it out and ran full npm lifecycle scripts from it: the publish
job next to VSCE_PAT/OVSX_PAT (and npm run publish:marketplace executes a
script from that same ref with the PATs in env), and the test job with NO
environment approval at all while inheriting the workflow-level
contents/packages/checks/pull-requests write grants. A dispatch pointing at
e.g. refs/pull/N/head would run outside-contributor code with the
marketplace keys behind one approval, or with a repo-write token behind
none.

Remove the input and hardcode the protected legacy-extension branch, drop
the workflow-level permissions to contents: read, and elevate only the
publish job to contents: write (tag push + GitHub release). The branch
input's default was legacy-extension, so normal publishes are unchanged.
publish-extension skill dispatch command updated to match.

* fix(vscode): SDK remote-config parity — refresh coordination, session gating, and fail-closed opt-out (#13226)

* feat(desktop): native notifications (#13166)

* feat(desktop): native notifications

* macos target

* fix(desktop): isolate macOS dev app identity

* fix(desktop): address notification review feedback

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched (#13310)

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched

Toggling an auto-approve setting while a task is open writes
autoApprovalSettings into the StateManager's task-settings overlay
(updateAutoApprovalSettings -> setTaskSettings). The SDK controller never
cleared that overlay on clearTask/showTaskWithId (the legacy controller
did), so after New Task the stale overlay kept shadowing global settings
in getGlobalSettingsKey(): toggle RPCs were accepted into global state,
but every posted state still carried the overlay's old version, which the
webview rejects as not newer - the auto-approve checkboxes froze forever.

Restore legacy parity in SdkTaskControlCoordinator: drop the overlay
(persisting pending writes first) in clearTask() and before installing a
different task's proxy in showTaskWithId().

Fixes #13260

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* changeset

* test(vscode): add end-to-end regression test for auto-approve freeze after New Task

Wires the real StateManager, the real updateAutoApprovalSettings handler,
and the real SdkTaskControlCoordinator.clearTask() together with the
webview's version gate modeled on ExtensionStateContext, pinning the
end-to-end invariant behind #13260: checkbox toggles must keep reaching
the webview after a mid-task toggle followed by New Task. Verified the
test fails when the clearTaskSettings() call is removed from clearTask().

* fix implicit any in regression test

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): show provider web-search support under the settings toggle (#13328)

* feat(desktop): show provider web-search support under the settings toggle

The global Web search toggle silently does nothing unless the session's
provider offers native web search, which made the setting read as if it
worked with any provider. The desktop General settings row now explains
that only providers with built-in web search honor it, and shows a live
status line: which connected providers are ready to use it (no extra
setup needed), or an amber warning with a link to the Models section
when none of them support it.

Support is resolved in the webview via a new providerOffersModelTool
helper in @cline/llms (browser export), sharing the same builtin-manifest
source of truth as the runtime's supportsModelTool attachment check.

* fix(desktop): address review — refetch web-search status on catalog invalidation, clarify per-model support

Greptile P2: the one-time catalog fetch could race an in-flight provider
save and show stale status; the row now refetches when the provider
catalog cache is invalidated (fired after saves complete).

Greptile P1: the ready line implied every model on the provider works;
Vertex excludes Claude routes, so the copy now scopes the promise to
models that support it.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* feat(ui/desktop): collapse finished runs into a work summary and remove hover-state dead space (#13315)

* feat(ui): add WorkActivity collapsed-run summary and float message actions as a pill

WorkActivity/WorkActivityTrigger/WorkActivityContent fold a finished agent
run's working rows (tool calls, thinking traces, narration) behind a single
"Worked for 4m 12s · 14 tool calls" disclosure built on the shared animated
disclosure primitives, with formatWorkActivityLabel/formatWorkDuration
exported for consumers.

Message hover actions no longer rely on the transcript reserving blank space
below each message: the action row is now a self-backed pill (border,
blurred background, shadow) that floats over whatever follows, so
conversations can pack rows tightly without hover chrome colliding with the
next message.

* feat(desktop): collapse finished runs into a work summary and tighten chat spacing

collapseCompletedWork post-processes the grouped transcript: once a run ends
on assistant text with no further tool calls, its working rows fold into one
expandable WorkActivity row while the final answer stays visible. Runs are
delimited by user messages; the trailing run only collapses when the session
has stopped running and actually produced an answer, so live streams and
cancelled/failed tails keep their rows. Assistant messages carrying images
or media are treated as deliverables and never collapse.

The conversation list gap drops from gap-8 to gap-4 now that hover actions
are self-backed pills that need no reserved space, and user messages add
their own top margin so turn boundaries stay visually distinct.

* refactor(ui/desktop): work summary label wording, flat expansion, stable in-run rhythm

Feedback round on #13315:

- Label reads "Worked for 4m 12s and made 14 tool calls" instead of joining
  with a dot; without a duration it falls back to "Made N tool calls".
- Expanded work rows render at transcript level — no rail or extra indent —
  since tool rows and thinking traces already carry their own nesting when
  expanded. The work content keeps the tight working-row rhythm.
- Live working rows (thinking traces + tool calls) now group into a 'run'
  render item with the same tight 0.25rem rhythm, so there is no oversized
  gap under a "Thought for Ns" row and every row keeps its exact position
  when the finished run folds into the work summary. A trailing
  answer-in-progress stays outside the group at transcript level, and pure
  prose spans keep normal spacing.
- The transient "Thinking..." indicator moves inside the transcript column
  and mirrors a trigger row's geometry, so the first real row replaces it in
  place with no jump.

* style(ui/desktop): hover-pill metrics, right-pointing work chevron, scroll and spacing fixes

Another feedback round on #13315:

- Hover action pill: +2px internal padding, a trailing inset after the
  timestamp (it sat flush against the pill border), and more clearance
  between the message content and the pill (2px -> 6px; the hover bridge
  grows to match).
- The work summary chevron points right while collapsed and continues
  counterclockwise to point up when expanded.
- Conversation bottom padding drops pb-20 -> pb-8: the composer sits below
  the scroller, so the padding only needs to clear a pinned action pill.
- Sending a message scrolls back to the bottom even if the reader had
  scrolled up (new AutoScrollOnSend on the user-message count, which ignores
  optimistic-bubble re-keying; @cline/ui now exports useConversation for
  this).
- An assistant answer directly under its run's working rows pulls itself
  0.5rem closer than the full transcript gap.

* style(desktop): leave a visible gap between a pinned action pill and the composer

pb-8 exactly matched the pill's ~40px footprint, so the last row's hover
actions sat flush against the composer top; pb-12 restores ~8px of daylight.

* style(desktop): widen the gap between the pinned action pill and the composer to ~24px

pb-12 left only ~8px of daylight under the pill; pb-16 reads comfortable
without reverting to pb-20's dead space.

* fix(desktop): keep the thinking indicator at the working-row offset mid-run

The indicator matched a trigger row's geometry but sat a full transcript gap
(1rem) below the last working row, while the tool/thinking row replacing it
joins the tight run group at 0.25rem — a visible upward jump. When the last
transcript item is working rows (or streamed assistant output), the
indicator now pulls up to the same tight offset; only at the start of a run,
under the user message, does it keep the normal gap.

* style(ui): calm the hover actions surface per team feedback

Borderless rectangle instead of the bordered pill: radius drops to
var(--radius), the side padding goes entirely (the icon buttons carry their
own hit areas), and the vertical padding halves. Blurred background and
shadow stay so it remains legible over following content.

* feat(ui/desktop): full-band hover reveal and iOS-style disclosure easing

The hover actions only appeared while the pointer was inside the message
box itself. The invisible bridge under each message now spans the full
height of the band the floating actions occupy (full row width), so
hovering anywhere in that strip reveals them. Sibling row types
(.cline-chat-tool, .cline-chat-work, and the desktop's run/tool groups)
become position: relative so they paint above the bridge — their own
content keeps its hover and clicks, and the bridge only wins in the band's
genuinely empty space.

All expandable rows (work summary, tool panels, thinking) open and close on
a 240ms symmetric ease-in-out cubic-bezier instead of the 60ms snap, with
chevron rotation on the same curve. Reduced-motion still disables both.

* revert(ui/desktop): drop the full-band hover reveal; quicken disclosure easing to 180ms

The full-band hover bridge (and the position: relative changes that made it
safe) is reverted per feedback — back to the narrow bridge that only spans
the gap under the message. The iOS-style ease-in-out on disclosures stays
but speeds up from 240ms to 180ms.

* fix(ui): recover live tool diffs that mount as a blank pierre skeleton

Live-streamed edit rows could show an empty diff for the whole run, with the
diff only appearing after the collapsed work row was expanded (fresh mount).
Root cause, confirmed by driving a live session and inspecting the element:
React StrictMode double-invokes @pierre/diffs' ref callback; the first
instance's async highlight work aborts on its immediate cleanup, and the
second instance adopts the abandoned half-rendered shadow tree as if it were
complete prerendered output — zero height, no code, no theme stylesheet,
permanently. A rendered diff always carries style[data-theme-css] in its
shadow root, so ToolFileDiff now checks for it shortly after mount and
remounts FileDiff (bounded attempts) when missing; the fresh host element
takes the normal render path and recovers within ~400ms. Verified live: the
diff now renders during the run.

* fix(desktop): keep interrupted runs expanded even with partial trailing text

The trailing-run collapse gated on 'ended with assistant text', which
misread a Stop that landed mid-answer as a finished run and folded the tool
calls the user wants to inspect. The gate is now the terminal status itself:
only completed (or restored-idle) sessions collapse the trailing run;
cancelled/failed/error tails keep their rows regardless of partial text.
(Greptile P1 on #13315 — matches the PR's stated rule.)

* feat(ui): share the markdown pipeline, chat polish, and ThinkingBlock across products (#13323)

* feat(ui): share the markdown pipeline, chat polish CSS, and ThinkingBlock

The desktop app and the cloud dashboard both consume @cline/ui yet rendered
assistant output differently, because Markdown policy and the thinking-trace
row lived app-side. This moves the shareable parts into the package:

- components/markdown (new export): the lazy Shiki code highlighter (GitHub
  light/dark, pinned language set) and agentMarkdownControls — the standard
  Streamdown configuration. streamdown/shiki/@shikijs/* become optional peer
  dependencies, mirroring @pierre/diffs.
- components/markdown.css: the desktop's chat polish moves in — chat-scale
  headings, outside list markers, single quiet code blocks with a
  hover-revealed copy control, table cards. Kept unlayered so it beats
  Streamdown's layered Tailwind utilities without !important.
- ThinkingBlock + formatThoughtLabel in agent-chat: the standard thinking
  row (brain icon, Thinking/Thought-for-Ns label, streaming shimmer, rail
  presentation, capped scrollable body). The shimmer and the
  reasoning-hover-suppression rule move into agent-chat.css; triggers gain
  the color transition the desktop applied locally.

Version bumps to 0.2.0-next.5 for the dashboard to pick up.

* refactor(desktop): consume shared markdown and thinking primitives from @cline/ui

The local Shiki highlighter, Streamdown controls, chat markdown polish CSS,
streaming-title shimmer, and reasoning hover-suppression rule are deleted in
favor of the @cline/ui versions (the highlighter test moves to the package's
suite). ReasoningBlock becomes a thin wrapper that hands MemoizedMarkdown to
the shared ThinkingBlock, and formatThoughtLabel re-exports from the package
so grouping code and tests keep their import path.

globals.css now imports @cline/ui/components/markdown.css (unlayered, so the
polish keeps beating Streamdown's layered utilities); the app keeps only what
is genuinely app-specific: link/image policy in markdown.tsx, selectability
rules, accent palettes, and the view-enter transition.

* style(ui/desktop): make thinking-trace prose legible

Thinking body text rendered too faint: plain muted-foreground plus the
desktop's font-thin weight. The shared thinking content now leans 75% of the
way back toward the body text color (still slightly de-emphasized), and the
desktop drops the thin font weight.

* ci(ui-publish): build @cline/shared before ui typecheck (#13354)

@cline/ui's generated-media imports @cline/shared/browser, which resolves to
shared's dist output. The build-shared step sat after typecheck/test/build,
so the first ui-publish dispatch since #13025 failed at Typecheck UI with
TS2307. Move the step to right after install.

* fix: run_commands object form without args routes through the shell instead of failing with ENOENT (#13336)

* fix: run_commands object form without args routes through the shell

The structured { command, args? } form of run_commands was always spawned
directly with shell: false. When a model emitted a full command line in
command with no args (e.g. { command: "echo hello" }), spawn failed with
ENOENT for any command containing a space, breaking command execution for
the whole session.

Direct exec now only applies when a non-empty args list is provided; the
object form without args is routed through getShellInvocation like the
string form. Schema descriptions are tightened so models put arguments in
args instead of embedding them in command.

Fixes #13279

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: trim structured-command schema descriptions

The union schema is only used for lenient validation of input the model
already sent; its descriptions never reach a model prompt. Keep them
short instead of restating executor behavior.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: simplify direct-exec comment in shell executor

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert: keep original structured-command schema description

The description never reaches a model prompt and the executor now handles
both shapes, so the wording change was cosmetic noise.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: gate direct exec on args key presence, not array length

Review feedback: an explicit empty args array is intentionally structured
input and stays direct exec; only an object with no args key is treated
as a full shell command line. Matches the key-presence rule already used
by the VS Code host's formatCommandForTerminal. Also replaces the
empty-args shell test (which was PowerShell-incompatible) with a test
pinning the direct-exec contract.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: normalize Gemini custom base URLs for legacy host-root values (#13329)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs: add GLM-5.3 to ClinePass models and reference pricing (#13357)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): stream run command output (#13179)

* feat(desktop): stream run command output

* fix(sdk): clean up detached command logs

* fix(sdk): reap detached logs after hub restarts

* fix(sdk): preserve live detached command logs

* fix(desktop): harden live command progress

* fix(sdk): recover detached logs for local hosts

* fix(desktop): reconcile command output tool rows

* fix(sdk): retain logs for surviving commands

* fix(core): prevent PID reuse from retaining detached logs

* fix(core): preserve detached logs on probe failures

* fix(core): retain detached logs during probe outages

* fix(desktop): resolve leftover merge conflict in messages projection test

Combine both sides of the assertion: main's incremented per-block
createdAt projection and this branch's toolCallId/hookEventName meta.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): make TUI dialog colors follow theme changes live (#13355)

* fix(cli): make TUI dialog colors follow theme changes live

Dialog content previously read the static palette constant, so open
dialogs (including the theme picker itself) kept the default dark-blue
accents while scrolling through theme previews. Add getDialogPalette /
useDialogPalette, which resolve dialog colors from the active theme's
dialog accents and re-render on every theme change, and migrate all
dialog-rendered components to it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(cli): derive dialog panel background from the active theme

Dark themes now lift their own background one OKLAB step for the dialog
surface, so panels keep the theme's hue instead of the library's fixed
#262626. DialogThemeSync pushes the surface into the dialog container
for new dialogs and repaints open panels, so the surface also follows
live theme previews. Light themes keep the neutral dark panel to match
the dark accent fallback and the light-on-dark dialog text.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: skill slash commands load via the skills tool instead of expanding into the user message (#13327)

* fix(desktop): show typed slash command instead of expanded skill markdown

The sidecar expands /skill and /workflow tokens into their instructions
before dispatching, so the runtime's persisted transcript only contains the
expanded text. After a turn (and when reopening a session) the webview
re-hydrates from that history and rendered the whole SKILL.md body as the
user's message; queue events echoing the expanded prompt could also add a
second user bubble, and fresh sessions were titled with the markdown's first
line. The CLI never shows this because its TUI keeps the typed text in its
own transcript and only sends the expanded prompt to the model.

Mirror that separation inside the desktop sidecar's display boundaries:

- history projection (readSessionMessages) inverts user text that starts
  with a configured command's instructions back to '/name remainder',
  which also repairs sessions recorded before this fix
- queue snapshots and chat_queued_prompt_start events echo the typed
  prompt recorded at expansion time, so the webview's optimistic-bubble
  re-key matches again
- an untitled session sent an expanded prompt gets titled from the typed
  command instead of the instructions' first line

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't overwrite a mid-turn rename with the typed-command title

The untitled check ran before dispatch, so renaming a fresh slash-command
session while its first turn was running got clobbered by the post-turn
typed-command title. Re-check at write time and only replace a missing title
or the one the runtime auto-derived from the expanded prompt.

Also documents the inherent prefix-inversion ambiguity flagged in review:
text hand-typed with a command's exact instructions persists byte-identically
to that command's expansion, so stored history alone cannot distinguish them.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop expanding skill commands; let the skills tool load them

Pasting the skill body into the prompt is why the transcript could ever show
it: the desktop webview re-hydrates from the runtime's persisted history, so
whatever the sidecar splices into the user message renders as if the user
typed it. The runtime already registers the skills tool, whose description
requires the model to invoke it whenever the user references a slash command
— so send the typed /skill text through and let the tool deliver the
instructions as a tool result (previously they arrived twice: pasted and via
the tool). The persisted user message, session title, and queue entries are
then simply the typed command, which deletes the typed-prompt registry, the
queue event/snapshot rewriting, and the title machinery from the previous
approach.

Workflows are not served by the skills tool and keep textual expansion, so
the read-time display inverter stays: it collapses expanded workflow prompts
— and skill prompts persisted before this change — back to the typed
/command in the history projection.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): option to keep skill slash commands typed for the skills tool

resolveRuntimeSlashCommandFromWatcher (and the hub snapshot proxy) accept
expandSkillCommands: hosts whose sessions register the skills tool pass
false so the typed /skill goes through and the model loads the instructions
as a tool result, keeping the persisted transcript as what the user typed.
Workflows always expand — the tool does not serve them. isSkillsToolAvailable
exposes the catalog check hosts use to decide (yolo preset and the skills
tool toggle leave textual expansion as the only delivery path).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): skill slash commands load via the skills tool instead of expanding

The TUI user-command wrap and buildUserInputMessage now keep a typed /skill
as-is when the session's mode/toggles register the skills tool, matching the
desktop app; workflows keep expanding, and yolo (zen) keeps expanding skills
because its preset has no skills tool. This also fixes CLI resume/history
surfaces showing the skill body: the persisted user message is now the typed
command.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): keep configured skill slash commands typed for the skills tool

expandSlashCommands no longer splices a configured skill's instructions into
the model text; the SDK session's skills tool delivers them as a tool result
(previously they arrived twice). Builtin pseudo-skills like /deep-planning
are not served by that tool and keep expanding, as do workflows.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): use the shared skill-expansion option in the sidecar

Replaces the sidecar's workflow-detection dance with core's
expandSkillCommands option and gates on isSkillsToolAvailable, restoring
textual expansion where the tool is missing (yolo mode or the skills tool
toggle) — a gap in the previous desktop-only change.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): drop the display inverter for expanded transcripts

Accepted trade-off to keep the change minimal: sessions recorded before
skills switched to the skills tool, workflow sends (deprecated), and
yolo-mode skill sends persist expanded instructions and now render that text
as-is instead of being collapsed back to the typed /command at projection
time.

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>

* Use fixed selection chevron in account dialog to match other dialogs (#13364)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): align system prompt with session mode (#13361)

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): finalize queued turns on chat_done with canonical history reconcile (#13330)

Turns that settle through the event stream (queued prompts, including the
first prompt of a fresh session) resolve their send() RPC early, so nothing
cleared the streaming shimmer or reconciled live-streamed content against
the persisted transcript at turn end. A turn whose deltas were incomplete
stayed visually streaming forever and only healed when a later non-queued
send rehydrated history.

chat_done (and chat_session_ended / the queue-drain double check) now clears
the active assistant streaming id and schedules a short-delayed
read_session_messages + applyCanonicalHistory, guarded by turn epoch,
session id, and in-flight send submissions so it never clobbers a newer
turn or duplicates the blocking send path's own finalization.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.14

* fix(clients): filter non-chat models from chat pickers (#13317)

* fix(clients): filter non-chat models from chat pickers

* fix(clients): align chat model eligibility

* fix(desktop): strip user_input envelope when copying a user message (#13369)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>

* docs: show DeepSeek V4 peak and off-peak pricing (#13312)

* docs: update DeepSeek V4 average pricing

* docs: show DeepSeek peak and off-peak pricing

* docs: add GLM-5.3 reference pricing (same as GLM-5.2)

* docs: add GLM-5.3 to ClinePass models table

* fix(llms): display billed gateway cost (#13385)

* fix(shared): run PowerShell commands with fail-fast error semantics (#13358)

* fix(shared): run PowerShell commands with fail-fast error semantics

The run_commands PowerShell wrapper never set $ErrorActionPreference, so
the default 'Continue' applied: a pipeline erroring per item (e.g. a
malformed Where-Object over Get-ChildItem -Recurse) emitted one error
record per enumerated file - tens of thousands of stderr records on
large trees, looking like a hang - and could still resolve as SUCCESS
with exit 0.

Prepend $ErrorActionPreference='Stop'; to the script content executed
by the ScriptBlock so the first error terminates the command with a
non-zero exit and a single error message. Concatenated on the same line
as the user command so error line numbers stay unshifted.

Fixes #13285

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(shared): set the fail-fast preference in the bootstrap scope

Setting $ErrorActionPreference='Stop' by string-prepending it into the
scriptblock source displaced a leading param(...) from its mandatory
first-statement position, so scripts beginning with a param block failed
with CommandNotFoundException. Preference variables are dynamically
scoped, so setting Stop in the -Command bootstrap gives the invoked
scriptblock identical fail-fast semantics while keeping the user script
byte-identical (param works, error positions unshifted) and drops the
doubled-quote escaping.

* docs(shared): document the fail-fast tradeoffs in the PowerShell wrapper

Stop promotes every non-terminating error, not only per-item pipeline
floods: partial-result commands (recursive listings over access-denied
junctions) now stop at their first error, and Windows PowerShell 5.1
turns in-script stderr redirection of succeeding native commands fatal.
State this in the wrapper comment as a deliberate tradeoff, with the
GitHub Actions precedent and the per-command opt-outs.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* ci: stop over-long changelogs from silently dropping release Slack posts (#12955)

Slack section blocks reject text longer than 3000 characters. The Slack
action logs that rejection as ##[error] but does not fail the step, so an
over-long changelog drops the release announcement while the run stays
green — cline@3.0.50 (3272 chars) published to npm, tagged, and cut a
GitHub release with no Slack post and nothing red to notice.

Every publish workflow pasted the changelog section verbatim into one
section block, so all six were exposed; the SDK, desktop, and extension
sections were only 150-350 chars under the ceiling.

Add a slack_content output alongside content: unchanged when the section
fits, otherwise trimmed on a line boundary with a link to the full
release notes. Only the Slack payload uses it — GitHub release bodies and
the desktop updater manifest still get the whole section.

* ci: tidy workflow cache config and job permissions (#13403)

Publish workflows now always do clean npm installs (no dependency
cache in their test gates), the e2e workflow's cache keys are
exact-match only, and the e2e job drops an id-token permission it
never used.

* Rename desktop app from "Cline Code" to "Cline" (#13401)

* Rename desktop app from Cline Code to Cline

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Format touched Rust test assertions

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): surface provider-executed tool activity as observational events (#13300)

* fix(llms): surface provider-executed tool activity as observational events

Provider-executed tool parts (e.g. every tool the Claude Code CLI runs
inside its own session) were dropped by the model-tool guard added for
web search: only declared model tools were re-emitted, everything else
hit continue with nothing yielded. Those sessions modified the workspace
with no tool activity in runtime events, transcripts, or the UI.

Route all providerExecuted parts onto the observational path instead:
emit execution-tagged tool-call-delta and tool-result events, matched by
tool-call ID for providers that omit the flag on the result half. They
stay out of AgentRuntime's execution/approval loop, and the runtime
already persists them as modelToolActivities and projects them for
display.

The AgentModelEvent tool-result variant widens toolName from
ModelToolName to string to carry the provider's own tool names.

* fix(agents): keep turns that are only provider-executed tool activity

A turn consisting solely of observational tool activity has an empty
assistant content array - the activity lives in message metadata, since
projecting it into content would replay tool_use blocks the model never
gets results for. The empty-content guard threw on such turns, erroring
the run and losing the activity from the transcript. Count model-tool
activity as content for the emptiness check (error finishes still
throw); replay stays safe through the codec's empty-content placeholder.
Also drop the trailing text delta from one gateway test so the tool-only
stream shape stays covered end to end.

* feat: allow agents to create scheduled tasks (#13331)

* feat(core, desktop): add durable todo agenda

* fix(desktop): secure todo approvals and track tool usage

* fix(desktop): clean up failed approval delivery

* fix(desktop): authenticate approval connections

* fix(desktop): cancel approvals on broadcast failure

* fix(desktop): authenticate development approvals

* fix(desktop): harden development approvals

* test(core): make task paths cross-platform

* fix(desktop): serialize approval readiness

* refactor(core): unify todo and schedule tools

* feat(core): distinguish user todos from agent suggestions

* fix(core): hide tasks tool in yolo mode

* fix(core): enforce schedule workspace scope

* fix(core): bind schedule scope to hub connection

* fix(core): establish task scope at hub startup

* fix(core): scope task automation by workspace

* test(core): normalize workspace path expectations

* test(core): serialize Windows CI workers

* fix(core): reject unregistered schedule authority

* fix(desktop): guard task execution commands

* fix(core): avoid polynomial regex in mention parsing

* fix(core): address schedule tool review feedback

* fix(core): bind websocket clients to hub workspace

* fix(core): flatten tasks tool input schema

* fix(core): authorize multi-workspace hub clients

* test(core): type hub transport authority mock

* fix(cli): register a workspace client for remote schedule commands (#13398)

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): treat ClinePass as OAuth-managed in the chat credential gate (#13404)

* fix(desktop): treat ClinePass as OAuth-managed in chat credential gate

ClinePass shares the Cline account OAuth credentials (its auth handler
stores under the "cline" provider), so the webview never sees a plain
API key for it. The chat pre-flight check only exempted cline/oca/
openai-codex, so switching to ClinePass while signed in via OAuth
blocked with "Missing API key" even though the sidecar resolves the
stored access token fine (which is why the CLI worked).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: format helpers.test.ts with biome

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): stack code block lines when streamdown lineNumbers is off (#13412)

streamdown renders each Shiki token line as a bare inline span with no
newline text between non-empty lines, and only applies its block line
class when lineNumbers is on. With lineNumbers off (the desktop app's
config) every multi-line fenced block collapsed into one run-on line.
Make the direct line spans under code-block-body display: block in the
shared markdown.css; empty lines keep their height via their lone "\n"
child under white-space: pre.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): work summary undercounts wall time when pre-tool thinking attaches to the answer (#13413)

* fix(desktop): anchor work summary duration on the answer row, not attached pre-tool reasoning

The collapsed 'Worked for Xs' row undercounted wall time whenever a turn's
assistant message contained thinking + tool_use with no narration text: the
canonical projection emitted the reasoning-only row after the tool row (both
stamped before the tool executed), the webview attached that row to the final
answer, and collapseCompletedWork used the answer's earliest attached
reasoning timestamp as the end anchor - excluding the entire tool execution
(e.g. 'Worked for 5s' for a turn with an 8s command).

- webview: end the work span at the answer row's own timestamp, clamped to
  the last collapsed row so a fallback answer bubble with a synthetic early
  timestamp cannot shrink the duration either
- sidecar: flush pending thinking before a tool_use row so rehydrated
  transcripts keep the live-stream order (thinking before its tool call) and
  pre-tool reasoning no longer rides on the next answer

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep interleaved thinking between the tool calls it separates

Address Greptile review: when one assistant message interleaves thinking
between multiple tool_use blocks, each reasoning segment now projects at its
own position (attached to a text row from its own segment when present,
otherwise as its own row) instead of merging into the first reasoning row,
which displayed later thinking before a tool call it actually followed.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): remove settings gear hover state while Account screen is open (#13408)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't show "No sessions found" while session history is still loading (#13414)

* fix(desktop): don't show 'No sessions found' while session history is still loading

Replace the isLoadingHistory flag with hasLoadedHistory, set only once the
backend has actually answered a list_discovered_sessions request. The sidebar
and Sessions view now keep their loading state until that first definitive
response, so the empty-state copy can no longer appear while history is still
being fetched (or while a failed fetch is being retried).

Also retry a failed initial fetch on the 2s event cadence instead of stranding
the UI until the 12s periodic poll, which is what stretched the misleading
empty state to ~10 seconds after a webview reload when the websocket lost the
race with the page load.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop history fast-retry from re-arming after hook unmount

A failed initial fetch that settles after the hook unmounted could schedule a
new retry timer after cleanup had already cleared the refs, leaving the
abandoned hook polling the backend every 2s. Guard scheduleRefresh with a
disposed ref set by the mount effect's cleanup.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix @ file mentions breaking on paths with spaces (#13391)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with "command not found" on VS Code 1.134 (#13402)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with 'command not found' on VS Code 1.134

Code action commands carried arguments (expandedRange, diagnostics),
which routes them through VS Code's CommandsConverter cache. VS Code
1.134 disposes the cached entries before the clicked action executes,
so every lightbulb action failed with 'Actual command not found,
wanted to execute cline.addToChat'.

Drop the arguments so the command id is passed through directly, and
recover the context in the handler instead: getContextForCommand now
expands an empty selection by 3 surrounding lines (matching the old
provider behavior) and gathers document diagnostics intersecting the
range when none are passed explicitly.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Scope gathered diagnostics to the selection/cursor

Match the old CodeActionContext.diagnostics behavior: only include
diagnostics intersecting the range the action was requested for, not
the surrounding lines the text gets expanded to.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop: unify Plugins, MCP, and Skills into one Plugins hub with a dedicated Marketplace page (#13411)

* Unify desktop plugins, apps, MCP, and skills into one Plugins hub with a Browse directory mode

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Open the marketplace directory as a modal over the Plugins hub instead of swapping the page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename directory to Marketplace: Browse Marketplace button, Marketplace modal title with icon, search placeholder

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix search input focus ring clipped by the Marketplace modal scroll container

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Address Greptile review: keep selected tag chip visible when its count drops to zero, and remount installed tab when a marketplace install completes after the modal closed

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Track marketplace modal mutation flag in a ref so a close click racing a queued render cannot skip the inventory remount

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make Marketplace its own settings page under Customizations and restore Channels as a standalone page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Remove icon from Marketplace page header for consistency with other settings pages

* Notify mounted inventory views when the marketplace invalidates the cache so late install completions refresh the Plugins hub

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop/ui): recommended and free model tiers in the composer model selector (#13410)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK (#13415)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): recommended-feed badges and descriptions in provider settings (#13416)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* feat(desktop): recommended-feed badges and descriptions in provider settings

Review suggestion on #13410: the provider settings page has room for
more model detail than the composer's picker. The cline/cline-pass
provider cards now refresh their model list through
list_provider_models (the catalog snapshot deliberately skips the
recommended-feed overlay so the startup catalog fetch never blocks on
the feed) and render Recommended/Free tier badges plus feed tags (NEW)
next to the model name, with the model description underneath. The
refreshed list also surfaces the live entries instead of the bundled
snapshot.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): scope the settings featured model list to its provider and revision

The fetched featured list was unscoped component state: switching
between cline and cline-pass reused the component instance, so the
previous provider's models stayed visible while the new request was
pending (or forever, when it failed), and the retained copy shadowed
later provider.modelList updates — adding a second custom model
submitted the stale list as the complete configuration and dropped the
first addition.

The fetched list now only applies to the provider and modelList
revision it was fetched for (falling back to the catalog snapshot
otherwise and refetching on membership changes), and add-model submits
the union of the displayed and configured ids so an update can never
silently unconfigure existing entries.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): update packed-Tailwind smoke contract for the picker's max-h-64 (#13421)

The ui-publish smoke check pins a set of Tailwind candidates the packed
sources must emit; #13410 grew the SearchCombobox options list from
max-h-56 to max-h-64, so the publish run failed on the stale candidate.
All other pinned candidates verified against the current sources.

* feat(desktop): refresh app icons and branding (#13400)

* ci(vscode): upload E2E failure recordings from the right path (#13427)

The job sets working-directory: apps/vscode, but that default applies to run
steps only, not to `uses:` steps. Since #10961 moved the extension under apps/
and added that default, the artifact path has resolved against the repo root,
matched nothing, and every failing run logged "No files were found with the
provided path: test-results/playwright/" instead of uploading recordings.

Widen to test-results/ so Playwright's error-context snapshots ship alongside
the videos.

* fix(hooks): deliver tool hook contextModification to the model (#13297)

* fix(hooks): deliver tool hook contextModification to the model

On the next engine, a tool_call (PreToolUse) hook's contextModification
was parsed into HookControl.context and then silently dropped: the
runtime beforeTool/afterTool result contract had no channel for
injecting conversation context. Legacy consumed it (ToolExecutor /
ToolHookUtils pushed <hook_context> blocks into the next user turn), so
this was a regression of documented behavior.

- Add appendContext to AgentBeforeToolResult/AgentAfterToolResult.
- AgentRuntime collects appendContext across hooks during an
  iteration's tool executions and appends one <hook_context> user
  message after the tool results, keeping tool-result parts contiguous.
- Map HookControl.context into appendContext in both subprocess hook
  layers (skipped when the hook cancels, matching legacy, where the
  message doubled as the error).
- Truncate injected context at 50KB per hook output, matching legacy.
- Concatenate appendContext across merged hook layers.

tool_result (PostToolUse) hooks still run detached with stdout ignored;
making them blocking so their context can be collected is a follow-up.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): stamp tool identity on injected hook context blocks

Contexts are batched into one message after the tool results, and
parallel tool execution collects them in completion order, so position
alone cannot attribute a block to its tool call. Add tool_name and
tool_call_id attributes to each <hook_context> block.

* fix(hooks): sanitize hook context block markup

Attribute values (tool_name, tool_call_id) are stripped of quote/angle
characters and embedded </hook_context> closers in hook output are
neutralized, so neither provider-supplied ids nor hook text can corrupt
or spoof a block's stamped identity.

* fix(hooks): neutralize forged opening hook_context tags in hook output

The previous sanitization only neutralized closing tags, so hook output
could still open a forged <hook_context> block claiming another tool's
identity. Escape both opening and closing embedded tags with one rule.

* fix(hooks): hide injected hook context from user-facing transcripts

Stamp the injected hook-context user message with displayRole 'system'
(the compaction-summary convention) so it reaches the model but does
not render as a user bubble in live or replayed transcripts. Without
this, resuming a session showed the raw <hook_context> block as if the
user had typed it.

* fix(hooks): neutralize case-variant embedded hook_context tags

The tag-neutralization regex was case-sensitive, so hook output could
still smuggle a forged tag as <HOOK_CONTEXT>. Match case-insensitively.

* fix(vscode): map PreToolUse contextModification into runtime appendContext

The extension's hooks adapter bridged file hooks into the SDK runtime
but forwarded only cancel/errorMessage, so a PreToolUse hook's
contextModification never reached the model. Map it into the runtime's
appendContext channel; HookFactory already truncates it at 50KB.

* fix(vscode): hide hook-injected context from replayed transcripts

Live sessions never rendered the injected <hook_context> user message,
but session reload replayed it as a user bubble (and post-resume turns
kept doing so). Treat these messages as synthetic in the user-message
mapping: honor the displayRole 'system' stamp the runtime sets, with a
text-prefix guard for paths where metadata is unavailable. This also
keeps edit/regenerate ordinal mapping aligned with visible bubbles.

* fix(hooks): run file hooks through exactly one layer per host

The VS Code extension registered two independent hook execution layers:
its own hooks adapter (config.hooks) and the SDK core's file-hook
extension from the runtime bootstrap. When both discover the same hook
files, every hook executes twice per event — and with context injection
wired, each contextModification would be injected twice.

Add a 'hooks' runtime config extension kind (in the default set, so the
CLI keeps core file hooks unchanged) and gate the bootstrap's file-hook
extension on it. The extension excludes 'hooks' at session start, so
its adapter — which also provides the hook status UI and the
hooksEnabled setting — is its single execution path.

* fix(vscode): discover hooks from the session workspace, not only global state

Hook discovery read workspaceRoots from global state shared across
every Cline instance, so another window repointing it made workspace
hooks silently stop being discovered. With the extension's adapter now
the single hook execution layer, that meant no hooks at all.

HookFactory takes an optional sessionWorkspaceRoot and unions that
root's .clinerules/hooks into discovery (and into cwd resolution), fed
from the session config's cwd. Shared-state discovery still works, so
behavior in the single-window case is unchanged.

* fix(hooks): keep sanitized hook attribute values distinguishable

Replacing every markup delimiter with the same underscore could
collapse two tool call ids that differ only by such a character into
identical stamps. Escape each delimiter with a distinct token instead.

* fix(hooks): make hook attribute sanitization injective

Escaping the underscore itself turns the attribute escaping into a
uniquely decodable code, so no two distinct tool call ids can collapse
to the same sanitized stamp (previously an id containing a literal
escape token could collide with an id containing the delimiter).

* fix(vscode): reconstruct hook status rows when replaying transcripts

hook_status messages are emitted live but never persisted, so reloading
a session dropped every hook row. The injected <hook_context> blocks
carry the hook source and tool name, so the replay translator now
rebuilds a completed hook status row from each block. The injection is
also no longer treated as a user turn boundary, so the final turn's
completion retag is unaffected by it.

* fix(hooks): collect PostToolUse hook output and honor its control (#13298)

* fix(hooks): collect PostToolUse hook output and honor its control

tool_result (PostToolUse) hooks ran fire-and-forget with stdout
ignored, so their entire JSON output — contextModification and cancel —
was discarded. Legacy awaited PostToolUse, injected its
contextModification into the conversation, and honored cancel.

- Run tool_result hook commands blocking (same 120s default timeout as
  tool_call) in both the hook-config-file layer and the agent-hook
  subprocess layer.
- Map their output: cancel stops the run with the hook's error message
  as the reason; otherwise context is injected via afterTool
  appendContext.

This restores legacy blocking semantics: tool results now wait for
tool_result hooks, but only in sessions that have one configured.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): bound tool_result hook wait and isolate cancel reason

Address review findings:
- The agent-hook subprocess layer forwarded an unset timeoutMs
  unchanged, so a tool hook command that never exits would block the
  agent indefinitely. Default both tool_call and tool_result to the
  120s bound the hook-config-file layer already used.
- A cancelling hook's error message was folded into the same context
  field as other hooks' injectable context, so merging controls could
  leak unrelated hook context into the cancellation reason. Carry it as
  a separate cancelReason, and surface it as the stop reason for
  beforeTool cancels too.

* fix(hooks): prefer errorMessage as a cancelling hook's stop reason

When a cancelling hook returns both contextModification and
errorMessage, the context-first parse precedence made the injectable
context the cancel reason and discarded the actual error. Parse the two
fields separately: errorMessage wins as the cancel reason (matching
legacy), and a lone errorMessage still folds into injectable context
for non-cancelling hooks as before.

* fix(vscode): honor PostToolUse hook cancel and contextModification

The adapter awaited PostToolUse hooks but discarded their output
entirely. Map cancel to a stop control (with errorMessage as the
reason) and contextModification into the runtime appendContext channel,
matching the PreToolUse mapping and legacy semantics.

* fix(hooks): whitespace-only errorMessage no longer suppresses the cancel reason

A cancelling hook returning meaningful context alongside a blank
errorMessage lost both: the parsers selected the whitespace as the
reason and the result mappers trimmed it away. Require a non-blank
errorMessage before it wins, so context serves as the fallback reason.
Apply the same fallback in the extension adapter's stop mapping.

* fix(core): stop Windows CI worker crashes from the agenda spec watcher (#13428)

* fix(core): watch agenda task specs via the resolved long path

fs.watch on a path with 8.3 short components (e.g. C:\Users\RUNNER~1
temp dirs) trips a libuv assertion in fs-event.c on Windows and aborts
the whole process. Since the agenda task manager landed, every hub
server test spins up its spec watcher on such a path on hosted Windows
runners, killing the vitest worker and failing the sdk-test Windows job
on every branch. Resolve the specs dir with realpathSync.native before
watching so libuv only ever sees the long form.

* test(ui): stub ResizeObserver for @pierre/diffs in tool-diff tests

jsdom does not implement ResizeObserver, so every ToolFileDiff render
logged a ReferenceError from @pierre/diffs to stderr. Tests still
passed; this just silences the noise the same way the constructable
stylesheet shim does.

* fix(core): skip the agenda spec watcher when the dir does not resolve

Falling back to the raw path on realpath failure would reintroduce the
Windows short-path abort; log and go without the watcher instead.

* fix(vscode): honor the classic truncation range when migrating legacy tasks (#13419)

Classic Cline truncated long conversations by omitting an index range of
api_conversation_history from every API request (keep the first
user-assistant pair, drop everything through the range end, strip
orphaned tool_results from the first kept message). The range was
persisted on the history item while the full history stayed on disk.

legacyApiHistoryToSdkMessages ignored conversationHistoryDeletedRange
and converted the entire file, so resuming a migrated long task handed
the SDK an untruncated working context that could exceed the model's
context window by millions of tokens - every request failed with
'prompt is too long' and every compaction restarted from the full
history (#12996, confirmed by the reporter: the task was migrated from
an older version and broke after a restart, with each compaction
starting from ~3M tokens).

The migration now replays exactly what the classic extension sent:
slice out the deleted range and drop orphaned tool_results, mirroring
ContextManager.getTruncatedMessages (see origin/main). Malformed ranges
fall back to the full history (previous behavior).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): show the diff edit view for multi-line edits in CRLF files (#13417)

The edit preview computed proposed content with an exact old_text match, but
the SDK executor normalizes old/new text to the file's own line endings before
matching (#12305) - reads strip CR, so models emit LF-only text even for CRLF
files. Any multi-line old_text in a CRLF file therefore failed the preview's
match: the diff edit view silently never opened while the executor applied the
edit. Single-line edits (no line break in old_text) were unaffected, which is
why the diff view appeared to trigger inconsistently.

Mirror the executor's EOL normalization (and its literal $-sequence insertion)
in the preview computation.

Fixes #13296

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): report truthful session status so desktop checkpoint restore stops wedging (#13418)

* fix(core): keep hub session status truthful across queue-drained turns

Queue-drained turns settle only through the event stream, but the hub
runtime host mistranslated their lifecycle in two ways:

- session.updated events carrying only a snapshot (persistence updates)
  defaulted the projected status to "running". When one trailed the
  final idle update after a turn, clients that track busy state from
  status events (the desktop sidecar's workspace restore gate) stayed
  busy forever. Use the snapshot's real status and emit nothing when
  neither source reports one.
- the per-run agent.done dedup was only reset by run.started, which the
  daemon-side queue drain never publishes, so a drained turn's done was
  swallowed as a duplicate of the previous turn's. Reset the dedup on
  session.pending_prompt_submitted, and suppress stale run.completed
  events that land inside a drained turn's window so they can neither
  emit a phantom done nor consume the drained turn's dedup slot.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(desktop): cover restore unlock after an event-settled queued turn

Exports the sidecar's core-session event handler so the queued-turn
lifecycle (busy via status events, cleared by the done agent event,
restore allowed afterwards) is testable end-to-end.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): start interactive sessions without a prompt as idle

The runtime host reported every new session as "running" until its
first turn ended. Interactive hosts (the desktop app) start sessions
with no prompt and dispatch turns through separate send calls, so a
created-but-never-prompted session stayed "running" forever — wedging
clients that gate workspace operations (checkpoint restore, message
edit) on active turns.

Interactive no-prompt starts now begin idle, start emits the session's
actual status (resumed sessions no longer masquerade as running), and
markTurn* transitions keep tracking in-memory status for lazily
persisted sessions so the first turn still reports running -> idle.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: format hub-runtime-host test filter

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop the drained-turn done bookkeeping, keep the minimal fix

The stuck restore is fully explained by the two status defects (fabricated
"running" from snapshot-only session.updated events, and never-prompted
interactive sessions reporting "running"). The done-dedup machinery for
queue-drained turns addressed a separate cosmetic gap (queued turns emit no
chat_done, pre-existing) and required fragile run-window heuristics, so it
is removed to keep this change reviewable. Sidecar test now settles the
queued turn through the status event, matching the shipped mechanism.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs(sdk): document the truthful session-status contract

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(deps): update Langfuse packages and bump app versions (#13443)

* chore(deps): update Langfuse packages and bump app versions

Update @langfuse/otel to v5.10.1 and add @langfuse/vercel-ai-sdk v5.9.1 for improved observability with Vercel AI SDK.

Bump versions for @cline/code to 0.0.14 and @cline/ui to 0.2.0-next.6, updated via bun.lock.

Other Changes:
Added optional userId to AgentRuntimeConfig.
Propagated userId, sessionId, conversationId, runId, iteration, provider, and model context into AI SDK telemetry.
Added AI SDK 7 runtimeContext with explicit includeRuntimeContext.
Added stable OTEL_SERVICE_NAME=cline-sdk.
Added runtime metadata assertions in agent tests.

* add taskId

* Revert "add taskId"

This reverts commit f20d31d96d.

* docs: simplify Open Cline step in installing guide (#13405)

* docs: remove duplicate GLM-5.3 rows in ClinePass tables (#13449)

Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>

* chore(sdk): release v0.0.76

* chore(cli): release v3.0.56

* docs(cli): scope the v3.0.56 release notes to CLI-visible changes

* feat(desktop): interactive welcome hero graphic (#13399)

* feat(desktop): add interactive welcome hero

* feat(desktop): support composable welcome hero variants

* feat(desktop): reskin first-run onboarding (#13441)

* refactor: centralize client tool availability (#13451)

* chore(sdk): release v0.0.77

* docs(cli): drop the tasks tool from the v3.0.56 notes, it is desktop-only

* chore(vscode): prepare 4.1.11 release

* chore(desktop): release v0.0.15

* fix(vscode): remote config MCP settings (#13466)

* fix(vscode): enforce enterprise MCP controls on the Customize marketplace

The unified Customize marketplace replaced the old MCP marketplace
without carrying over enterprise remote-config enforcement: the catalog
RPC returned every MCP entry and installs were never policy-checked,
so orgs with mcpMarketplaceEnabled=false or an allowedMCPServers
allowlist saw (and could install) all marketplace MCP servers.

- Filter MCP entries out of getMarketplaceCatalog when the marketplace
  is disabled, and restrict entries to the allowlist when configured
  (matching entry id, display name, installed server name, or source
  repo URL, mirroring legacy GitHub-URL allowlist ids)
- Reject installMarketplaceEntry requests that violate the policy
- Map the published catalog's repo/homepage fields onto
  sourceUrl/homepageUrl so URL-based allowlists can match
- Update the enterprise MCP server controls docs

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: simplify MCP marketplace policy enforcement

Fold the policy check into marketplace-helpers, drop the dedicated
test suite, and trim the docs edit to the strictly necessary line.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Treat an empty preserved capability list as unspecified when seeding tools (#13465)

* Treat an empty preserved capability list as unspecified when seeding tools

toSdkModelInfo guarded the tools seeding with a strict
preservedCapabilities === undefined check, but modelHasCapability —
the runtime's own reader — treats undefined AND length === 0 as
"unspecified". A custom OpenAI-Compatible model whose stored
capabilities field is a defined-but-empty array (a config carried over
from before the field existed, or one round-tripped through a boundary
that defaults it to []) skipped the seeding; the first boolean
projection to run afterwards (e.g. supportsReasoning) then populated
the array, the runtime gate read the non-empty, tool-less list as
authoritative, and every tool definition was silently dropped from the
session (#13463).

The guard now covers the empty array too, matching the reader's
unspecified semantics.

* test: satisfy the store's isModelInfo gate so the empty-capabilities case actually reaches knownModels

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: yzxcj797 <yzxcj797@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>

* chore(vscode): prepare 4.1.12 release

* Add feature flags to the desktop app (#13289)

* Add feature flags to the app

* React to account updates

* Address comments

* use a per-app file

* fix: propagate Langfuse session telemetry (#13473)

* fix telemetry session propagation

* feat telemetry client version metadata

* fix(core): address Langfuse review feedback — hub client identity + delegated agent session grouping (#13475)

* fix(core): rebuild hub session client identity from request headers

Hub-backed sessions do not transport extensionContext (it is local-only),
so the daemon's runtime built traces without the clientName/clientVersion
metadata even though the hub client bakes X-CLIENT-TYPE / X-CLIENT-VERSION
into the session's provider headers. Reconstruct extensionContext.client
from those headers during local runtime bootstrap so hub-backed Langfuse
traces carry the same client identity as local runtimes, and the daemon's
header re-resolution stops clobbering the original X-CLIENT-TYPE.

* fix(core): propagate parent distinctId/sessionId to delegated agents

Delegated agents (spawned sub-agents, configured agents, teammates) were
built without distinctId and sessionId, so their Langfuse traces had no
userId or sessionId and did not group with the parent user or session.
Thread the host-resolved distinctId through RuntimeBuilderInput and the
root sessionId through the delegated-agent config provider, and copy both
onto the delegated AgentConfig.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* ci(vscode): make combined nightly manual-dispatch only

The PublishNightly environment gained required reviewers, so each cron
run parked on approval, held the workflow's concurrency group, and
silently cancelled every scheduled run queued behind it. 20 consecutive
scheduled nightlies died this way between 2026-07-31 and 2026-08-21;
the only nightlies that shipped in that window were manual dispatches.

Drop the cron rather than leave a trigger that cannot succeed unattended.

* feat(hub): add drain and upgrade commands with replay support (#13468)

* feat(hub): add drain and upgrade commands with replay support

* handles disconnection

* feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport

Completes the wiring the previous commits' primitives needed:
HubServerTransport gains isDraining(), hub.drain/hub.status/profile.get
command handling, and replayEventsAfter() (backed by the durable event
log), plus the sequence/sinceSequence wire types they depend on in
shared/hub.ts. run-queue-handlers.ts reads the active bot profile's
plugin roots when executing durable runs.

Also adds hub/profiles/: profile.json (identity/rules/plugins) ->
system prompt composition, --profile / CLINE_HUB_BOT_PROFILE
resolution, and the bundled cline-dad profile with its
cline_hub_support read-only diagnostics tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Revert "feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport"

This reverts commit 6696d5d202.

* fix(hub): dedupe replayed events by eventId, not just sequence

HubEventLogStore.append() returns a new envelope stamped with a
sequence rather than mutating the input, so a pending approval
re-issued sequence-less by subscribe() (it predates any durable-log
append) and its later sequence-stamped copy from the durable log are
two different objects carrying the same eventId. The replay-then-live
buffer in browser-websocket.ts only deduped by sequence, so the
sequence-less copy's guard never tripped and it was delivered a second
time when the buffer flushed after replay.

Track delivered eventIds alongside the sequence cursor; eventId
survives the append/stamp round-trip unchanged, so this dedupes the
exact-same logical event regardless of which copy arrives first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): wire drain, durable event log, and run queue into the live transport

CI on this branch failed bun run build:sdk: browser-websocket.ts,
client/index.ts, and hub-websocket-server.ts (already on this branch)
reference sequence/sinceSequence, HubServerTransport.isDraining(), and
the "hub.drain" command — but the commit that reverted bot profiles
out of this branch also reverted this wiring, since it shared a commit
with the profiles work. That wiring is a hub concern, not a
bot-profiles one; split it back out.

- shared/hub.ts: sequence/sinceSequence types, run.enqueue/run.list/
  hub.drain/hub.status/stream.replay capability, command, and event
  names. profile.get intentionally excluded — stays bot-profiles-only.
- context.ts: isDraining() on HubTransportContext. botProfile field
  intentionally excluded.
- hub-server-transport.ts: eventLog/runQueue fields and start/stop
  lifecycle, publish() appends to the durable log, handleCommand cases
  for run.enqueue/run.list/hub.drain/hub.status, drain-refusal check,
  replayEventsAfter()/lastEventSequence(). startBotProfile()/
  startHubSupportTool() and the profile.get case intentionally
  excluded.
- run-queue-handlers.ts: added without handleProfileGet (needs
  ctx.botProfile, which doesn't exist here).
- hub-upgrades.test.ts: added without its two bot-profile-injection
  tests (they need a resolved bot profile to assert against).

Verified bun run build:sdk exits 0 (the exact CI command) and
bunx vitest run src/hub passes (311/312; the one failure is the
same pre-existing environment-timing flake already present before
this change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): export instance-lock, event-log, and run-queue from the hub barrel

These landed as internal modules only; hub-server-transport.ts and
hub-websocket-server.ts import them by direct path, but nothing
re-exported them from the public @cline/core/hub surface the way
sibling discovery/server modules already are.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): wire the instance lock into the daemon entry point

The singleton lock (discovery/instance-lock.ts) and its consumption in
startHubWebSocketServer/ensureHubWebSocketServer were already on this
branch, but the daemon entry point's own half was not: retrying a bind
when a retiring predecessor still holds the lock, and exiting with a
distinct code (3) instead of the generic fatal path when a live Hub
already owns the data directory. Without this, a daemon racing a
retiring predecessor could fail outright instead of waiting the lock
out, and losing the singleton race looked identical to a crash.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(hub): address drain/upgrade review findings (#13478)

- cline hub upgrade: check idleness at least once (--wait 0 works), reject
  non-numeric --wait, and un-drain on every abort path so an aborted
  upgrade can never leave the hub refusing new work
- add cline hub drain --off and the off query param to requestHubDrain so
  POST /drain?off is reachable from shipped code
- HubEventLogStore/HubRunQueue: WAL journal mode + busy_timeout, and stamp
  sequences from lastInsertRowid instead of SELECT MAX(sequence)
- HubInstanceLock.acquire: degrade to an unheld lock when SQLite is
  unavailable instead of refusing hub startup; only BUSY/LOCKED still
  raises HubLockHeldError
- ensureHubWebSocketServer: retire an unusable discovered hub through the
  shared retireDiscoveredHub (busy hubs are attached to, drain precedes
  shutdown, discovery cleared only when the hub actually retired)
- replay adapter: advance the cursor past eventId-deduped events, cap
  replay pages, stop when the cursor stalls, and drop the dedupe set after
  the buffered flush so it cannot grow for the socket lifetime

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* fix(hub): derive the singleton e2e challenger cwd portably

The challenger's working directory was derived by round-tripping the
discovery path through a file: URL and stripping the last pathname
segment. On Windows that yields a POSIX-style '/C:/...' path, which is
not a valid spawn cwd, so the spawn fails ENOENT before the singleton
lock is ever contested and the Windows SDK test job goes red.

The data dir is simply the discovery file's parent: use dirname().

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): stop stored capability lists from silently revoking tool calling for custom models (#13476)

* fix(core): seed tools capability when custom model capabilities are synthesized from boolean flags

For a models.json entry with no explicit capabilities list, toStoredModelInfo
synthesized a capability array purely from boolean convenience flags (e.g.
supportsReasoning: true -> ["reasoning"]). modelSupportsToolCalling fails open
only for a missing or empty list, so the synthesized non-empty list read as an
authoritative denial and silently stripped every tool definition from requests
to custom OpenAI-compatible models (#13463).

Seed "tools" whenever the list was not explicitly authored and the boolean
projections made it non-empty, preserving the fail-open contract. Explicitly
authored capability lists remain authoritative and can still disable tools.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(core): cover stale catalog capability overrides

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: treat stored capability lists as non-authoritative for tool calling

The hasExplicitCapabilities guard still let two producers of tool-less
lists through:

- The VS Code legacy-override migration (legacyModelInfoToOverrides)
  persists explicit partial lists like ["prompt-cache"] into models.json
  for custom OpenAI-compatible models, which then read as an authoritative
  "cannot call tools" and drop every tool - same symptom as #13463.
- Any hand- or UI-authored partial list on a non-catalog model.

Stored entries and user-authored provider metadata have no way to declare
"cannot call tools" (there is no supportsTools field, and every writer
that authors a full list includes "tools"), so seed "tools" into any
non-empty list for a language model. Only generated catalog capabilities
remain authoritative - a genuine no-tools catalog model stays that way -
and non-language models (e.g. image generation) never gain a tools claim.

Also make legacyModelInfoToOverrides write "tools" into the arrays it
fabricates, matching the providers.json migration, so models.json stops
being poisoned for older readers.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): prepare 4.1.13 release

* chore(sdk): release v0.0.78

* chore(cli): release v3.0.57

* fix(core): run hub e2e files serially so daemon timing budgets survive CI contention

singleton.e2e.test.ts (added in #13468) spawns real daemons and runs for
~15s. Vitest's default file parallelism let it run alongside
shutdown.e2e.test.ts, whose assertions are wall-clock bound: discovery
within 10s, exit within 5s, and a 2s shutdown watchdog. On the 2-core
windows-latest runner that contention alone broke those budgets, failing
the shutdown test two different ways across runs — once never observing
discovery, once with the daemon forced to exit before its HTTP 202
flushed (socket hang up). The test passed on Windows before #13468 and
has failed every SDK publish run since.

* chore(desktop): release v0.0.16

* test(sdk): give windows-sensitive suites realistic timeouts

Four consecutive SDK publish runs failed on windows-latest, each on a
different test, all of them plain timeouts: two @cline/shared SQLite
tests at the 5s vitest default, core's bash executor at 10s, and the hub
singleton endpoint test at 10s. The 2-core Windows runner spawns forks
and takes SQLite locks slowly enough to blow those budgets under load.

These timeouts guard against hangs; they are not timing assertions (the
one suite that does assert elapsed time, shutdown.e2e, was fixed by
removing file-level parallelism instead). Raise core to 20s and give
@cline/shared an explicit 15s in place of the inherited 5s default.

* fix(telemetry): emit task.completed from every session teardown path (#13489)

The task.completed fallback lived only inside shutdownSession, but
stopSession/dispose route interactive sessions with a terminal reported
status through releaseSessionRuntime, which never emitted. Truthful
session-status reporting (shipped in 4.1.11) re-routed a large share of
interactive stops onto that branch and silently dropped the event.

Route the emission through a single choke point,
emitTaskCompletedOnTeardown, called from both shutdownSession and
releaseSessionRuntime. The completion criterion no longer reads
session.status: interactive sessions use the recorded final-turn
outcome (lastInteractiveTurnFinishReason), non-interactive sessions
keep the existing input.status === "completed" logic. A new
taskCompletedEmitted flag (also set by the submit_and_exit observer)
enforces exactly one task.completed per session. failSession now
records the errored final turn so a stale "completed" from an earlier
turn can never leak into the teardown emission. Telemetry only; no
user-facing behavior changes.

* chore(vscode): release v4.1.14

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on (#13498)

* fix(vscode): honor MCP auto-approve settings for SDK tool calls

The SDK extension required both the global 'Use MCP servers' auto-approve
toggle AND each tool's per-tool autoApprove flag before silently approving
an MCP call, while the legacy extension treated them as either/or. Restore
the legacy OR semantics so toggling MCP auto-approve works again.

Also key toolPolicies by the registered SDK tool name (via
defaultMcpToolNameTransform, now exported from @cline/core) instead of raw
server__tool. Servers whose names contain sanitized characters (e.g.
marketplace names like github.com/user/repo) or exceed 64 chars produced
policy keys that never matched the registered tool, so those MCP tools ran
without any approval gate; the live auto-approve lookup now re-applies the
transform instead of string-splitting the name.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Revert "fix(vscode): honor MCP auto-approve settings for SDK tool calls"

This reverts commit 86c568fbba.

* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on

The SDK extension only auto-approved an MCP call when the global 'Use MCP
servers' auto-approve toggle AND that tool's per-tool autoApprove flag were
both set, so toggling MCP auto-approve appeared to do nothing and users had
to opt in each tool individually. The toggle alone now governs all MCP
tools; the per-tool flag is no longer consulted.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): release v4.1.15

* fix(cli): remove the $4.99 ClinePass promo copy (#13514)

The $4.99 first-month promo is ending, so the CLI's first-launch "Try ClinePass" dialog should no longer advertise it. Also drops the leftover CLI_PROMO_CODE plumbing, which has been an empty string since the promo-code flow was removed.

* fix(vscode): resolve hook workspace identity from the window, not shared global state (#13352)

* fix(vscode): resolve hook workspace identity from the window, not shared global state

Hook discovery, hook cwd selection, and the workspaceRoots metadata passed
to hook scripts all read the workspaceRoots/primaryRootIndex global state
keys. Global state lives in ~/.cline and is shared by every Cline instance
(all VS Code windows, the CLI, the JetBrains plugin), and nothing writes
these keys anymore, so hooks resolved against whatever project some other
or older instance last recorded. With a second window open on another
project, a workspace's .clinerules/hooks scripts were never discovered.

Resolve workspace roots via a single guarded helper backed by
HostProvider.workspace.getWorkspacePaths() (in-process, window-scoped,
same as refreshHooks): blank paths are filtered, a host-bridge failure
degrades to no workspace roots instead of silently disabling global hooks
or skipping blocking PreToolUse guards, and one resolution is threaded
through hooks-dir discovery, cache misses, cwd selection, and hook input
metadata so they can't disagree (previously up to four host lookups per
hook execution — real gRPC round trips in the standalone host). Roots and
hooks dirs are matched on whole path segments with the longest root
winning, so prefix-sharing or nested workspace roots resolve to the right
project. The adapter creates the runner once per event and skips no-op
runners, making creation the single resolution point; the separate
hasHook/getHookInfo checks are removed. The dead workspaceRoots and
primaryRootIndex state keys are dropped, and the four hand-rolled
HostProvider.workspace test stubs are consolidated into one shared
helper.

* test(vscode): add e2e coverage for workspace-scoped hook discovery

Boots real VS Code with the packaged extension against the workspace
fixture, sends a prompt, and asserts the fixture's UserPromptSubmit hook
was discovered from the open window's workspace, executed with that
workspace root as its cwd, and received the same root in its
workspaceRoots input — the end-to-end contract the hook workspace
identity fix establishes.

* test(vscode): isolate the e2e hook fixture from the shared workspace

The UserPromptSubmit fixture hook lived in the shared e2e workspace, so
every prompt-sending spec executed it (hooksEnabled defaults to true) —
and its cold PowerShell spawn on Windows pushed chat.test.ts past the
5s expect timeout. hooks.test.ts now overrides workspaceDir to a
dedicated workspace-hooks fixture, so only the hooks spec pays the hook
spawn.

* fix(hub): cap hub-events db size so it can't fill the disk (#13516)

* fix(hub): cap hub-events db size so it can't fill the disk

Row/time retention alone didn't bound disk usage: envelopes carrying
full session snapshots reach hundreds of KB each, so retained rows
could total tens of GB, sweeps only ran hourly, and DELETE never
shrinks a SQLite file. Enforce a 64 MiB size budget in prune() (oldest
rows first, VACUUM to return the space), and also prune after every
16 MiB appended so bursts can't outrun the hourly timer.

Fixes #13505

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): tolerate VACUUM failure on a full disk

VACUUM needs scratch space and can fail in exactly the state a
ballooned event log causes. The byte-budget deletes already bound live
data, so swallow the error and let the next sweep retry the reclaim
instead of aborting startup pruning and disabling the durable log.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): count the size budget in UTF-8 bytes, not characters

envelopeJson.length (UTF-16 units) and SQLite LENGTH() (characters)
undercount multibyte text by up to 3x, which could leave a CJK-heavy
log settled above budget and re-running VACUUM every sweep. Use
Buffer.byteLength and LENGTH(CAST(... AS BLOB)) instead.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(sdk): carry root overrides into the Node smoke-test sandbox (#13517)

ci-node-smoke.ts installs the packed SDK tarballs with a plain npm
install in a fresh temp dir, where the repo root package.json overrides
do not apply. When @sap-cloud-sdk 4.9.0 shipped (2026-08-24) it broke
@sap-ai-sdk/ai-api 2.14.0 (via @jerome-benoit/sap-ai-provider in
@cline/llms) with ERR_PACKAGE_PATH_NOT_EXPORTED, failing the smoke step
on every PR even though the root already pins @sap-cloud-sdk/* to 4.6.0.

Copy the root overrides block into the generated sandbox package.json
so the smoke install resolves the same pinned versions as the repo and
future third-party releases cannot break it independently.

* chore(sdk): release v0.0.79

* fix(vscode): don't steal last-used provider from ClinePass on credential refresh (#13520)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(hub): flush the /shutdown 202 before daemon teardown

The /shutdown handler queued teardown on a microtask, which runs before
the event loop's write phase, so the daemon could process.exit() before
the accepted 202 was handed to the socket. Unix masked it (uv_try_write
lands small loopback writes synchronously); Windows has no such fast
path and lost the race regularly — the recurring shutdown.e2e.test.ts
'socket hang up' failures on windows-latest. Start teardown from the
response's write callback instead, with an idempotent 1s fallback so a
client that vanishes mid-write cannot strand the daemon, and send
Connection: close so the client gets a FIN rather than an abort.

Since the flakiness this compensated for is fixed at the source, restore
maxWorkers: 2 for the Windows core suite (serializing it cost ~3 min of
CI per run), and raise the e2e daemon discovery hang guard 10s→30s —
it guards against hangs, not runner speed.

* chore(cli): release v3.0.58

* fix(core): prevent search_codebase from crashing the process on giant single-line files (#13525)

* fix(core): prevent search_codebase from crashing the process on giant single-line files

searchWithRipgrep buffered all of rg's --json stdout into one string. Each
JSON event embeds the full text of the matched line (--max-columns is
ignored in JSON mode), so searching a directory of serialized trace dumps
(single-line multi-hundred-MB JSON files) accumulated gigabytes of stdout
until string concatenation threw RangeError: Out of memory inside the
stream data handler. That throw is outside the tool's try/catch, so it
escalated to an uncaughtException and killed the CLI/hub daemon.

Parse rg's JSON events incrementally line by line, drop events larger
than 256KB, truncate matched/context lines to MAX_LINE_CHARS, and stop
reading once maxResults is reached. The fallback regex scan now skips
files larger than 10MB (reporting the skip count) and truncates its
context lines the same way.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* simplify search_codebase crash fix to a minimal diff

Replace the incremental JSON-event parser with three small guards: stop
buffering rg stdout past 10MB, drop the trailing partial event before
parsing, and slice fallback context lines to MAX_LINE_CHARS. Drops the
fallback file-size skip and skip-count reporting.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag (#13522)

* chore(vscode): remove per-tool MCP auto-approve checkboxes from webview

MCP auto-approval is now governed solely by the global 'Use MCP servers'
toggle; the SDK approval path (shared with the CLI and desktop app) has no
per-tool granularity, so the per-tool and 'Auto-approve all tools'
checkboxes were no-ops that implied control that no longer exists. Remove
them from the MCP settings view and chat tool rows. The autoApprove arrays
in cline_mcp_settings.json and the toggleToolAutoApprove RPC are left
intact for the legacy extension.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag

Keep the checkbox components, handlers, and RPC plumbing intact but gate
rendering behind SHOW_MCP_PER_TOOL_AUTO_APPROVE=false: the SDK approval
path (shared with the CLI and desktop app) is all-or-nothing via the
global 'Use MCP servers' toggle, so the per-tool checkboxes were no-ops.
Flip the flag back on if the SDK gains per-tool approval granularity.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(tools): create new files with the platform-native line ending (#13521)

* fix(tools): use platform-native EOL for new files and preserve CRLF in apply_patch updates

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* simplify to the minimal new-file EOL fix

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* extract shared normalizeNewFileLineEndings helper

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add suggested schedule templates to the desktop Schedules page (#13529)

* Add suggested schedule templates to desktop Schedules page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix unreadable selected text in inputs caused by selection utility conflict

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Restyle Suggested section label as small gray uppercase

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Hide suggested schedule cards that match an existing schedule name

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Disable the agent todo tool and hide the Agenda UI in the desktop app (#13530)

* remove todo tool and Agenda UI, keep schedule-only tasks tool

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: biome formatting fixes

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* restore agenda backend; disable todo kind behind a flag instead of deleting

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* keep agenda automation pump idle while the todo tool is disabled

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* remove todo tool and Agenda UI altogether (revert the disable-flag hybrid)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* restore all agenda code to main state

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* disable agent todo tool and hide Agenda UI behind flags

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Add Desktop App and Cloud Platform to bug report issue template (#13532)

* Add Desktop App and Cloud Platform to bug report surfaces

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename Surface Diagnostics field to Diagnostics

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: sidebar navigation cleanup with New/Schedule/Customize rows and dialog-based search (#13533)

* desktop: clean up sidebar navigation chrome

- Give New Task its own full-width labeled row below the logo row
  instead of an ambiguous icon next to the agenda toggle
- Wire the New Task row to the home action so starting a new task
  clearly takes you home (the logo still works as a fallback)
- Swap back/forward chevrons for browser-style arrow icons and
  bump their size

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: sidebar New/Schedule/Customize rows and always-visible search

- Stack New (plus icon), Schedule, and Customize as full-width labeled
  rows below the logo; whole row highlights on hover via sidebarItem
- New starts a fresh task (home), Schedule opens Settings > Schedules,
  Customize opens the Customizations sections (Plugins first)
- Show the session search bar permanently above the sessions list
  instead of hiding it behind a search icon toggle

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: move session search into a dialog behind a logo-row icon

- Replace the inline sidebar search bar with a search icon in the
  logo row that opens a cmdk command dialog listing sessions
- Selecting a result opens that session and closes the dialog
- Remove the agenda/tasks toggle the icon replaces, along with the
  now-unreachable sidebar Agenda panel (the welcome screen still
  surfaces agenda tasks)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* desktop: load full session history when the search dialog opens

Addresses Greptile review on #13533: the dialog only searched the
currently loaded history batch, so older unloaded sessions could not
be found. Opening search now kicks off loadAllSessions() (the hook's
purpose-built global-search loader), and the empty state reads
'Searching older sessions...' while more history is streaming in.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Hide Channels and Agents sections from desktop app sidebar (#13527)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop app: organize sidebar sessions into Pinned, Scheduled, and Tasks sections (#13528)

* Add Pinned/Scheduled/Tasks categories to desktop app sidebar

Replace the Schedules and Favorites filter-menu options with visible
collapsible category sections in the session sidebar, and rename the
Favorite action to Pin across the sidebar and sessions view.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Grow full history window when Tasks show-more outpaces loaded tasks

loadMoreSessions treats its argument as a limit on all sessions, but the
Tasks show-more count only tracks Task rows, so once pinned/scheduled
rows pushed the loaded total past the requested count the call no-oped
and clicks went dead. Grow the whole history window via
loadOlderSessions instead, and only when the loaded tasks cannot fill
the next page. Addresses Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Auto-fill the Tasks page instead of fetching once per show-more click

A single 50-session window growth can consist entirely of pinned or
scheduled sessions, leaving a show-more click with no visible Tasks
progress. Replace the one-shot fetch with a page-fill effect that keeps
growing the history window until the requested Tasks page fills or
history runs out. Addresses the follow-up Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Halt page-fill retries after a failed history fetch

A failed fetch leaves the task count and has-more state unchanged,
which are exactly the conditions the page-fill effect fires on, so one
failing request would retry and re-toast forever. Halt the effect after
a failure and let the next explicit show-more click retry. Addresses
the third Greptile review on #13528.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Redesign desktop Model Providers page and split voice input into its own settings page (#13531)

* Redesign desktop Model Providers page and split voice input into its own settings page

- Group providers into Connected / Popular / All with auth-kind hints and
  connection status instead of per-row enable toggles
- Show browser sign-in (not an API key field) for OAuth providers, with a
  collapsed manual-key escape hatch where supported, plus explicit
  Connect / Disconnect / Sign out actions
- Move voice input to a dedicated Settings > Voice page that only offers
  connected transcription-capable providers, preselects a default model
  (streaming preferred), and stays disabled in the sidebar until a
  provider is connected

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Show native tooltip on the disabled Voice settings nav item

Disabled buttons drop pointer events, so the 'connect a model provider'
hint moves to a wrapping span for the browser tooltip to render.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Drop letter avatars and gray provider ids from provider rows and voice chips

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Drop model counts from provider list rows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename provider Connected status to Configured and drop the green styling

A settings entry is configuration, not a live connection; neutral gray
text avoids implying an active link, since the user still picks which
configured provider to use per chat.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Resync provider catalog from disk when a settings save fails

Connect/disconnect/credential edits update the list optimistically; a
failed save now reloads the catalog instead of leaving the optimistic
state (and the view's module cache) claiming a configuration that was
never persisted.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename oauthProvider test fixture to dodge CodeQL name heuristic

CodeQL's clear-text-storage query flags any identifier matching 'oauth'
as a credential source and traced the fixture's provider id into the
favorite-models localStorage write, which stores only provider/model id
strings. Renaming the fixture removes the false-positive source.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Guard catalog reloads against races and resync detail drafts on failed saves

Optimistic provider mutations now bump a generation that discards any
in-flight catalog response, so a failed-save recovery reload can't
overwrite a newer edit with an older disk snapshot. The recovery also
remounts the provider detail panel via a reset token so its local field
drafts reflect the reloaded on-disk state instead of unpersisted edits
or an optimistically cleared disconnect.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix failed-save recovery ordering and retry superseded reloads

Remount the provider detail only after the authoritative catalog reload
lands, so its drafts re-seed from disk state rather than the optimistic
values that failed to persist. When a concurrent edit supersedes the
recovery's in-flight response, retry the reload (bounded) instead of
dropping it, since that edit performs no reload of its own.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): Customize hub, sidebar overhaul, and settings polish (#13538)

* feat(desktop): merge customization pages into a Customize hub with inline marketplace

Replaces the Plugins page and the dedicated Marketplace page with a single
Customize hub. Tabs: Skills, MCP, Plugins, Rules, Hooks, Tools, each with
live counts. Tabs backed by a marketplace catalog render the installed
items followed by an inline browsable Browse section (CLI-hub style), so
installing from the catalog immediately reflects in Installed above.

- Installed cards restyled to mirror the browse-card anatomy: bg-card p-4
  containers, absolute top-right xs Uninstall matching Install, truncating
  semibold titles, primary-tinted icons, real Badge components instead of
  ad-hoc bordered spans, un-indented line-clamped descriptions
- Rules/Hooks/Tools rows brought into the same card language; redundant
  intro paragraphs (duplicating the page description) removed; Tools group
  headers match the Installed header style with counts
- Marketplace section header renamed to Browse; duplicate 'N results' row
  removed (the header count is the single source)
- MCP embedded view now shows the full marketplace instead of
  installed-only

* feat(desktop): overhaul sidebar sessions and navigation

Sessions list:
- Sort toggle removed; sessions are always grouped by project, with pinned
  sessions leading each group (both subsets ordered by recency). The
  Pinned/Scheduled/Tasks category sections and their time-mode paging
  machinery (page-fill effect included) are deleted
- Scheduled sessions get an inline clock icon next to the pin position;
  pin + clock render together when both apply, and the running/unread
  status dot now coexists with them
- One font size (text-sm) across the list: titles, timestamps, project
  headers, show-more buttons, empty states. sidebarText needed !text-sm
  because the default button size's text-base wins the twMerge conflict
- Gradient fade under the Sessions header once the list scrolls, so rows
  fade out instead of hard-clipping
- The session-detail hover card is controlled from the sidebar and closes
  on scroll (Radix receives no pointer events while scrolling, so it used
  to float over moving content)
- Sidebar min resize width raised 224->260px; the per-project show-more
  label truncates so its nowrap text can't force rows to overflow and clip
  timestamps at narrow widths

Navigation:
- Customize replaces the Plugins/Marketplace/Hooks/Rules/Tools sidebar
  entries; Schedules and Customize are hidden from the expanded settings
  nav (their top rows cover them) but stay reachable when collapsed
- The settings gear always opens General instead of resuming the last
  section; the Account no-op hover special case is gone
- The New row highlights (aria-current) while the fresh not-yet-started
  task page is showing and hands off to the session row once the task
  starts; hitting New also focuses the prompt input via a window-event
  signal (lib/prompt-input-focus.ts) since the sidebar and composer sit in
  distant subtrees
- Fixed the xs button size collapsing any icon-bearing button to 12x12
  (leftover has-[>svg]:size-3 from when xs was a micro button) — this was
  why Uninstall buttons rendered broken next to Install

* feat(desktop): polish settings pages and chat composer

Models page:
- The provider detail panel is always open: no X button, no empty
  no-selection state. It defaults to the first connected provider (falling
  back to the first in the catalog), which also removes the layout shift
  that happened when the page swapped between full-width and panel
  variants on selection
- Fixed the list pane becoming unscrollable while the panel was open:
  grid items default to min-size auto, so the pane grew past its track
  inside the overflow-hidden grid and its ScrollArea had nothing to
  scroll; wrapped it in a min-h-0 min-w-0 cell
- Add Provider opens a Dialog instead of swapping the page
  (AddProviderContent gained a dialog variant that renders only the form)
- Embedded inputs (provider search, model search, detail fields) share one
  EMBEDDED_INPUT_CLASS stripping the Input component's own border/dark bg
  tint/shadow/ring, which rendered as a mismatched inner box; the model
  search box uses the same h-9/px-3 frame as the provider search
- Model list flows with the page instead of a max-h capped inner scroller

Other pages:
- Account uses the shared PageFrame/PageHeader: left-aligned, text-3xl
  title, Sign Out in the header actions slot
- Desktop notifications is one General section: header row plus the
  Event/Notify/Sound matrix nested in a card, so its rows no longer read
  as top-level peers of Dark mode; 'Available in the desktop app' label
  removed
- Schedule page retitled from Schedules with a real description; Customize
  description rewritten

Chat composer:
- The voice dictation button only renders once a voice model is
  configured (Settings -> Voice); the unconfigured deep-link state is
  gone (prop type kept for an easy restore)

* chore(desktop): release v0.0.17

* fix(desktop): unblock sdk-test lint on the voice-input model picker (#13553)

The model picker renders a radiogroup of styled buttons with role=radio
and aria-checked; biome's useSemanticElements flags the role as an
error, which fails the sdk-test Quality Checks lint for every PR
touching sdk/ or apps/ paths. Suppress with a justification — switching
to input type=radio needs a restyle and belongs to the desktop settings
work.

* fix(vscode): include rich workspace metadata in system prompt (#13518)

* capture richer workspace information for vs code extension

* fix(shared): redact credentials from workspace remotes

* fix(shared): avoid regex backtracking in remote redaction

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>

* Hide task costs on vscode when ClinePass is selected (#13515)

* fix: stop showing cost estimates for subscription-billed providers (#13552)

* fix(vscode): stop showing cost estimates for subscription-billed providers

Providers whose usage is covered by a flat-rate subscription (ChatGPT
Plus/Pro via openai-codex, ClinePass) are marked with
metadata.usageCostDisplay = "subscription" in the SDK, and the CLI
already suppresses dollar figures for them. The VS Code host collapsed
that value into "show" before it reached the webview, so the task
header and model pricing rows rendered API-rate cost estimates that
users read as real charges on top of their subscription.

Pass all three usageCostDisplay values ("show" | "hide" |
"subscription") through the catalog listing and render cost only when
the value is "show", matching the CLI's shouldShowCliUsageCost
policy.

* feat(llms): mark Claude Code as a subscription-billed provider

Claude Code is typically authenticated with a Claude Pro/Max
subscription, but its models reuse Anthropic API pricing metadata, so
Cline rendered per-token prices and API-rate cost estimates for usage
that is covered by the subscription. Set usageCostDisplay =
"subscription" on the provider (picked up by the CLI and the VS Code
webview) and suppress the price rows in the Claude Code settings card.

The Claude Code CLI can also run on API-key billing, where a real cost
exists; the provider cannot distinguish the two, so we prefer showing
no number over a misleading one.

* fix(vscode): suppress cost display until provider listings load

While the ListProviders request is in flight (or after it fails), the
usage-cost hook had no listing to consult and fell back to "show",
flashing the API-rate estimate at subscription users on every chat-view
mount — the exact display the previous commit removes. Return
"unknown" whenever listings are absent; consumers already render cost
only for "show", so they suppress it during that window with no
changes. Briefly hiding a real cost is harmless, briefly showing a fake
charge is not.

* fix(desktop): reconcile voice settings after main sync

* test(llms): allow experimental ElevenLabs models

* fix(sdk): preserve canonical media model behavior

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: JasmineLCY <38378321+JasmineLCY@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Haley Park <haleypark.design@gmail.com>
Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>
Co-authored-by: yzxcj797 <54314860+yzxcj797@users.noreply.github.com>
Co-authored-by: yzxcj797 <yzxcj797@users.noreply.github.com>
Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
2026-08-25 14:10:57 -07:00
John Choi bba15e8782 chore(desktop): cut 0.0.16-beta.1 (#13462) 2026-08-21 12:19:53 -07:00
+3 b905c78640 chore(desktop): sync latest main into desktop experimental (#13461)
* fix(vscode): continue the surviving session on resume instead of rebuilding with the original task text (#13175)

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilt the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

The preserved conversation history is the source of truth on resume, so
the fallback prompt now just asks the model to reassess the history and
continue, matching the legacy resume prompt which also never resent the
original task. User-typed text still takes precedence when provided.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): continue the surviving idle session on follow-ups instead of rebuilding

Stopping a turn keeps the session alive, but every idle follow-up (bare
Resume after Stop, and typed follow-ups after a completed turn) tore
that session down and rebuilt it from persisted task history before
sending. Continue the matching idle session in place instead, the same
way the CLI reuses the live session after an abort. Rebuilding from
history now only happens when no live session matches the displayed
task (task opened from history, extension host reload).

A bare resume still needs a prompt to start a turn, so it sends the
neutral [TASK RESUMPTION] prompt (shared with the rebuild fallback and
hidden from the transcript); user-typed content is echoed and sent
as-is. If the send lands while the abort is still settling, the runtime
auto-queues it and drains once the abort completes.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): consolidate follow-up send paths in SdkFollowupCoordinator

Now that idle follow-ups continue the live session in place, the
two-mode sendToActiveSession helper was redundant: its non-queued branch
duplicated continueIdleSession minus the bare-resume prompt. Split it
into a single-purpose queueToActiveSession and fold the idle no-task
send into continueIdleSession, flattening askResponse's decision tree
to: queue onto a running turn, continue a matching live idle session,
rebuild from history, or abandon.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): reuse the existing neutral resumption prompt for bare resumes

Drop the newly invented long resumption wording in favor of the phrase
that already existed as the no-history fallback and that the transcript
hiding logic and test fixtures recognize: '[TASK RESUMPTION] Please
continue where you left off.' The net change to resumeSessionFromTask
against main is now just deleting the branch that resubmitted
historyItem.task as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilds the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

Bare resumes now always use the neutral prompt that already existed as
the no-history fallback; user-typed text still takes precedence. This
matches the legacy resume prompt (responses.taskResumption), which only
ever included user-supplied text as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): hide synthetic prompts from the queued-prompt echo

A send that races a settling abort is auto-queued by the runtime, so a
bare Resume can reach the pending_prompt_submitted echo carrying the
synthetic [TASK RESUMPTION] prompt. Echoing it leaked model-facing text
as a visible user bubble and shifted the visible-user-message ordinals
that edit/regenerate mapping relies on. Filter synthetic prompts with
isSyntheticUserPrompt, keeping user attachments visible (matching
isSyntheticSdkUserMessage semantics).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): preserve LiteLLM input token limits (#13293)

* fix(vscode): preserve LiteLLM input token limits

* fix(vscode): prefer live LiteLLM model metadata

* fix(vscode): generalize private catalog metadata

* test(vscode): preserve llms exports in vscode lm mock

* fix(vscode): point provider signup URLs at their API key pages (#13337)

* fix(vscode): point Mistral signup URL at the general API keys console

The Mistral provider's signup link led to the Codestral console, which
issues Codestral-scoped keys that fail with 401 on api.mistral.ai — the
endpoint the provider actually calls. Point it at the general API keys
page instead.

Fixes #13288

* fix(vscode): deep-link DeepSeek and Fireworks signup URLs to their API key pages

Both pointed at marketing homepages; link straight to the key-creation
pages instead, matching the rest of the registry and the desktop app's
provider-key-urls map.

* fix(ci): always build the legacy bundle from the legacy-extension branch (#13349)

The combined-VSIX workflow took legacy-ref as a free-form dispatch input
with no publish-time validation (next-ref has one: publish requires main).
Any typed ref — a PR merge ref, an unprotected branch — would be built
into the published VSIX by the environment-less build job, and the publish
environment approver only ever sees an opaque prebuilt artifact, so the
approval protected the marketplace PAT but not the shipped bytes.

Remove the input entirely and hardcode the protected legacy-extension
branch, which makes that branch's protection rules load-bearing for
releases. The tested-sha pinning between test-legacy and build is
unchanged. publish-extension skill dispatch command updated to match.

* fix(ci): lock the legacy publish workflow to the legacy-extension branch (#13350)

The branch dispatch input was a free-form string with no validation. Both
jobs checked it out and ran full npm lifecycle scripts from it: the publish
job next to VSCE_PAT/OVSX_PAT (and npm run publish:marketplace executes a
script from that same ref with the PATs in env), and the test job with NO
environment approval at all while inheriting the workflow-level
contents/packages/checks/pull-requests write grants. A dispatch pointing at
e.g. refs/pull/N/head would run outside-contributor code with the
marketplace keys behind one approval, or with a repo-write token behind
none.

Remove the input and hardcode the protected legacy-extension branch, drop
the workflow-level permissions to contents: read, and elevate only the
publish job to contents: write (tag push + GitHub release). The branch
input's default was legacy-extension, so normal publishes are unchanged.
publish-extension skill dispatch command updated to match.

* fix(vscode): SDK remote-config parity — refresh coordination, session gating, and fail-closed opt-out (#13226)

* feat(desktop): native notifications (#13166)

* feat(desktop): native notifications

* macos target

* fix(desktop): isolate macOS dev app identity

* fix(desktop): address notification review feedback

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched (#13310)

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched

Toggling an auto-approve setting while a task is open writes
autoApprovalSettings into the StateManager's task-settings overlay
(updateAutoApprovalSettings -> setTaskSettings). The SDK controller never
cleared that overlay on clearTask/showTaskWithId (the legacy controller
did), so after New Task the stale overlay kept shadowing global settings
in getGlobalSettingsKey(): toggle RPCs were accepted into global state,
but every posted state still carried the overlay's old version, which the
webview rejects as not newer - the auto-approve checkboxes froze forever.

Restore legacy parity in SdkTaskControlCoordinator: drop the overlay
(persisting pending writes first) in clearTask() and before installing a
different task's proxy in showTaskWithId().

Fixes #13260

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* changeset

* test(vscode): add end-to-end regression test for auto-approve freeze after New Task

Wires the real StateManager, the real updateAutoApprovalSettings handler,
and the real SdkTaskControlCoordinator.clearTask() together with the
webview's version gate modeled on ExtensionStateContext, pinning the
end-to-end invariant behind #13260: checkbox toggles must keep reaching
the webview after a mid-task toggle followed by New Task. Verified the
test fails when the clearTaskSettings() call is removed from clearTask().

* fix implicit any in regression test

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): show provider web-search support under the settings toggle (#13328)

* feat(desktop): show provider web-search support under the settings toggle

The global Web search toggle silently does nothing unless the session's
provider offers native web search, which made the setting read as if it
worked with any provider. The desktop General settings row now explains
that only providers with built-in web search honor it, and shows a live
status line: which connected providers are ready to use it (no extra
setup needed), or an amber warning with a link to the Models section
when none of them support it.

Support is resolved in the webview via a new providerOffersModelTool
helper in @cline/llms (browser export), sharing the same builtin-manifest
source of truth as the runtime's supportsModelTool attachment check.

* fix(desktop): address review — refetch web-search status on catalog invalidation, clarify per-model support

Greptile P2: the one-time catalog fetch could race an in-flight provider
save and show stale status; the row now refetches when the provider
catalog cache is invalidated (fired after saves complete).

Greptile P1: the ready line implied every model on the provider works;
Vertex excludes Claude routes, so the copy now scopes the promise to
models that support it.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* feat(ui/desktop): collapse finished runs into a work summary and remove hover-state dead space (#13315)

* feat(ui): add WorkActivity collapsed-run summary and float message actions as a pill

WorkActivity/WorkActivityTrigger/WorkActivityContent fold a finished agent
run's working rows (tool calls, thinking traces, narration) behind a single
"Worked for 4m 12s · 14 tool calls" disclosure built on the shared animated
disclosure primitives, with formatWorkActivityLabel/formatWorkDuration
exported for consumers.

Message hover actions no longer rely on the transcript reserving blank space
below each message: the action row is now a self-backed pill (border,
blurred background, shadow) that floats over whatever follows, so
conversations can pack rows tightly without hover chrome colliding with the
next message.

* feat(desktop): collapse finished runs into a work summary and tighten chat spacing

collapseCompletedWork post-processes the grouped transcript: once a run ends
on assistant text with no further tool calls, its working rows fold into one
expandable WorkActivity row while the final answer stays visible. Runs are
delimited by user messages; the trailing run only collapses when the session
has stopped running and actually produced an answer, so live streams and
cancelled/failed tails keep their rows. Assistant messages carrying images
or media are treated as deliverables and never collapse.

The conversation list gap drops from gap-8 to gap-4 now that hover actions
are self-backed pills that need no reserved space, and user messages add
their own top margin so turn boundaries stay visually distinct.

* refactor(ui/desktop): work summary label wording, flat expansion, stable in-run rhythm

Feedback round on #13315:

- Label reads "Worked for 4m 12s and made 14 tool calls" instead of joining
  with a dot; without a duration it falls back to "Made N tool calls".
- Expanded work rows render at transcript level — no rail or extra indent —
  since tool rows and thinking traces already carry their own nesting when
  expanded. The work content keeps the tight working-row rhythm.
- Live working rows (thinking traces + tool calls) now group into a 'run'
  render item with the same tight 0.25rem rhythm, so there is no oversized
  gap under a "Thought for Ns" row and every row keeps its exact position
  when the finished run folds into the work summary. A trailing
  answer-in-progress stays outside the group at transcript level, and pure
  prose spans keep normal spacing.
- The transient "Thinking..." indicator moves inside the transcript column
  and mirrors a trigger row's geometry, so the first real row replaces it in
  place with no jump.

* style(ui/desktop): hover-pill metrics, right-pointing work chevron, scroll and spacing fixes

Another feedback round on #13315:

- Hover action pill: +2px internal padding, a trailing inset after the
  timestamp (it sat flush against the pill border), and more clearance
  between the message content and the pill (2px -> 6px; the hover bridge
  grows to match).
- The work summary chevron points right while collapsed and continues
  counterclockwise to point up when expanded.
- Conversation bottom padding drops pb-20 -> pb-8: the composer sits below
  the scroller, so the padding only needs to clear a pinned action pill.
- Sending a message scrolls back to the bottom even if the reader had
  scrolled up (new AutoScrollOnSend on the user-message count, which ignores
  optimistic-bubble re-keying; @cline/ui now exports useConversation for
  this).
- An assistant answer directly under its run's working rows pulls itself
  0.5rem closer than the full transcript gap.

* style(desktop): leave a visible gap between a pinned action pill and the composer

pb-8 exactly matched the pill's ~40px footprint, so the last row's hover
actions sat flush against the composer top; pb-12 restores ~8px of daylight.

* style(desktop): widen the gap between the pinned action pill and the composer to ~24px

pb-12 left only ~8px of daylight under the pill; pb-16 reads comfortable
without reverting to pb-20's dead space.

* fix(desktop): keep the thinking indicator at the working-row offset mid-run

The indicator matched a trigger row's geometry but sat a full transcript gap
(1rem) below the last working row, while the tool/thinking row replacing it
joins the tight run group at 0.25rem — a visible upward jump. When the last
transcript item is working rows (or streamed assistant output), the
indicator now pulls up to the same tight offset; only at the start of a run,
under the user message, does it keep the normal gap.

* style(ui): calm the hover actions surface per team feedback

Borderless rectangle instead of the bordered pill: radius drops to
var(--radius), the side padding goes entirely (the icon buttons carry their
own hit areas), and the vertical padding halves. Blurred background and
shadow stay so it remains legible over following content.

* feat(ui/desktop): full-band hover reveal and iOS-style disclosure easing

The hover actions only appeared while the pointer was inside the message
box itself. The invisible bridge under each message now spans the full
height of the band the floating actions occupy (full row width), so
hovering anywhere in that strip reveals them. Sibling row types
(.cline-chat-tool, .cline-chat-work, and the desktop's run/tool groups)
become position: relative so they paint above the bridge — their own
content keeps its hover and clicks, and the bridge only wins in the band's
genuinely empty space.

All expandable rows (work summary, tool panels, thinking) open and close on
a 240ms symmetric ease-in-out cubic-bezier instead of the 60ms snap, with
chevron rotation on the same curve. Reduced-motion still disables both.

* revert(ui/desktop): drop the full-band hover reveal; quicken disclosure easing to 180ms

The full-band hover bridge (and the position: relative changes that made it
safe) is reverted per feedback — back to the narrow bridge that only spans
the gap under the message. The iOS-style ease-in-out on disclosures stays
but speeds up from 240ms to 180ms.

* fix(ui): recover live tool diffs that mount as a blank pierre skeleton

Live-streamed edit rows could show an empty diff for the whole run, with the
diff only appearing after the collapsed work row was expanded (fresh mount).
Root cause, confirmed by driving a live session and inspecting the element:
React StrictMode double-invokes @pierre/diffs' ref callback; the first
instance's async highlight work aborts on its immediate cleanup, and the
second instance adopts the abandoned half-rendered shadow tree as if it were
complete prerendered output — zero height, no code, no theme stylesheet,
permanently. A rendered diff always carries style[data-theme-css] in its
shadow root, so ToolFileDiff now checks for it shortly after mount and
remounts FileDiff (bounded attempts) when missing; the fresh host element
takes the normal render path and recovers within ~400ms. Verified live: the
diff now renders during the run.

* fix(desktop): keep interrupted runs expanded even with partial trailing text

The trailing-run collapse gated on 'ended with assistant text', which
misread a Stop that landed mid-answer as a finished run and folded the tool
calls the user wants to inspect. The gate is now the terminal status itself:
only completed (or restored-idle) sessions collapse the trailing run;
cancelled/failed/error tails keep their rows regardless of partial text.
(Greptile P1 on #13315 — matches the PR's stated rule.)

* feat(ui): share the markdown pipeline, chat polish, and ThinkingBlock across products (#13323)

* feat(ui): share the markdown pipeline, chat polish CSS, and ThinkingBlock

The desktop app and the cloud dashboard both consume @cline/ui yet rendered
assistant output differently, because Markdown policy and the thinking-trace
row lived app-side. This moves the shareable parts into the package:

- components/markdown (new export): the lazy Shiki code highlighter (GitHub
  light/dark, pinned language set) and agentMarkdownControls — the standard
  Streamdown configuration. streamdown/shiki/@shikijs/* become optional peer
  dependencies, mirroring @pierre/diffs.
- components/markdown.css: the desktop's chat polish moves in — chat-scale
  headings, outside list markers, single quiet code blocks with a
  hover-revealed copy control, table cards. Kept unlayered so it beats
  Streamdown's layered Tailwind utilities without !important.
- ThinkingBlock + formatThoughtLabel in agent-chat: the standard thinking
  row (brain icon, Thinking/Thought-for-Ns label, streaming shimmer, rail
  presentation, capped scrollable body). The shimmer and the
  reasoning-hover-suppression rule move into agent-chat.css; triggers gain
  the color transition the desktop applied locally.

Version bumps to 0.2.0-next.5 for the dashboard to pick up.

* refactor(desktop): consume shared markdown and thinking primitives from @cline/ui

The local Shiki highlighter, Streamdown controls, chat markdown polish CSS,
streaming-title shimmer, and reasoning hover-suppression rule are deleted in
favor of the @cline/ui versions (the highlighter test moves to the package's
suite). ReasoningBlock becomes a thin wrapper that hands MemoizedMarkdown to
the shared ThinkingBlock, and formatThoughtLabel re-exports from the package
so grouping code and tests keep their import path.

globals.css now imports @cline/ui/components/markdown.css (unlayered, so the
polish keeps beating Streamdown's layered utilities); the app keeps only what
is genuinely app-specific: link/image policy in markdown.tsx, selectability
rules, accent palettes, and the view-enter transition.

* style(ui/desktop): make thinking-trace prose legible

Thinking body text rendered too faint: plain muted-foreground plus the
desktop's font-thin weight. The shared thinking content now leans 75% of the
way back toward the body text color (still slightly de-emphasized), and the
desktop drops the thin font weight.

* ci(ui-publish): build @cline/shared before ui typecheck (#13354)

@cline/ui's generated-media imports @cline/shared/browser, which resolves to
shared's dist output. The build-shared step sat after typecheck/test/build,
so the first ui-publish dispatch since #13025 failed at Typecheck UI with
TS2307. Move the step to right after install.

* fix: run_commands object form without args routes through the shell instead of failing with ENOENT (#13336)

* fix: run_commands object form without args routes through the shell

The structured { command, args? } form of run_commands was always spawned
directly with shell: false. When a model emitted a full command line in
command with no args (e.g. { command: "echo hello" }), spawn failed with
ENOENT for any command containing a space, breaking command execution for
the whole session.

Direct exec now only applies when a non-empty args list is provided; the
object form without args is routed through getShellInvocation like the
string form. Schema descriptions are tightened so models put arguments in
args instead of embedding them in command.

Fixes #13279

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: trim structured-command schema descriptions

The union schema is only used for lenient validation of input the model
already sent; its descriptions never reach a model prompt. Keep them
short instead of restating executor behavior.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: simplify direct-exec comment in shell executor

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert: keep original structured-command schema description

The description never reaches a model prompt and the executor now handles
both shapes, so the wording change was cosmetic noise.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: gate direct exec on args key presence, not array length

Review feedback: an explicit empty args array is intentionally structured
input and stays direct exec; only an object with no args key is treated
as a full shell command line. Matches the key-presence rule already used
by the VS Code host's formatCommandForTerminal. Also replaces the
empty-args shell test (which was PowerShell-incompatible) with a test
pinning the direct-exec contract.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: normalize Gemini custom base URLs for legacy host-root values (#13329)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs: add GLM-5.3 to ClinePass models and reference pricing (#13357)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): stream run command output (#13179)

* feat(desktop): stream run command output

* fix(sdk): clean up detached command logs

* fix(sdk): reap detached logs after hub restarts

* fix(sdk): preserve live detached command logs

* fix(desktop): harden live command progress

* fix(sdk): recover detached logs for local hosts

* fix(desktop): reconcile command output tool rows

* fix(sdk): retain logs for surviving commands

* fix(core): prevent PID reuse from retaining detached logs

* fix(core): preserve detached logs on probe failures

* fix(core): retain detached logs during probe outages

* fix(desktop): resolve leftover merge conflict in messages projection test

Combine both sides of the assertion: main's incremented per-block
createdAt projection and this branch's toolCallId/hookEventName meta.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): make TUI dialog colors follow theme changes live (#13355)

* fix(cli): make TUI dialog colors follow theme changes live

Dialog content previously read the static palette constant, so open
dialogs (including the theme picker itself) kept the default dark-blue
accents while scrolling through theme previews. Add getDialogPalette /
useDialogPalette, which resolve dialog colors from the active theme's
dialog accents and re-render on every theme change, and migrate all
dialog-rendered components to it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(cli): derive dialog panel background from the active theme

Dark themes now lift their own background one OKLAB step for the dialog
surface, so panels keep the theme's hue instead of the library's fixed
#262626. DialogThemeSync pushes the surface into the dialog container
for new dialogs and repaints open panels, so the surface also follows
live theme previews. Light themes keep the neutral dark panel to match
the dark accent fallback and the light-on-dark dialog text.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: skill slash commands load via the skills tool instead of expanding into the user message (#13327)

* fix(desktop): show typed slash command instead of expanded skill markdown

The sidecar expands /skill and /workflow tokens into their instructions
before dispatching, so the runtime's persisted transcript only contains the
expanded text. After a turn (and when reopening a session) the webview
re-hydrates from that history and rendered the whole SKILL.md body as the
user's message; queue events echoing the expanded prompt could also add a
second user bubble, and fresh sessions were titled with the markdown's first
line. The CLI never shows this because its TUI keeps the typed text in its
own transcript and only sends the expanded prompt to the model.

Mirror that separation inside the desktop sidecar's display boundaries:

- history projection (readSessionMessages) inverts user text that starts
  with a configured command's instructions back to '/name remainder',
  which also repairs sessions recorded before this fix
- queue snapshots and chat_queued_prompt_start events echo the typed
  prompt recorded at expansion time, so the webview's optimistic-bubble
  re-key matches again
- an untitled session sent an expanded prompt gets titled from the typed
  command instead of the instructions' first line

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't overwrite a mid-turn rename with the typed-command title

The untitled check ran before dispatch, so renaming a fresh slash-command
session while its first turn was running got clobbered by the post-turn
typed-command title. Re-check at write time and only replace a missing title
or the one the runtime auto-derived from the expanded prompt.

Also documents the inherent prefix-inversion ambiguity flagged in review:
text hand-typed with a command's exact instructions persists byte-identically
to that command's expansion, so stored history alone cannot distinguish them.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop expanding skill commands; let the skills tool load them

Pasting the skill body into the prompt is why the transcript could ever show
it: the desktop webview re-hydrates from the runtime's persisted history, so
whatever the sidecar splices into the user message renders as if the user
typed it. The runtime already registers the skills tool, whose description
requires the model to invoke it whenever the user references a slash command
— so send the typed /skill text through and let the tool deliver the
instructions as a tool result (previously they arrived twice: pasted and via
the tool). The persisted user message, session title, and queue entries are
then simply the typed command, which deletes the typed-prompt registry, the
queue event/snapshot rewriting, and the title machinery from the previous
approach.

Workflows are not served by the skills tool and keep textual expansion, so
the read-time display inverter stays: it collapses expanded workflow prompts
— and skill prompts persisted before this change — back to the typed
/command in the history projection.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): option to keep skill slash commands typed for the skills tool

resolveRuntimeSlashCommandFromWatcher (and the hub snapshot proxy) accept
expandSkillCommands: hosts whose sessions register the skills tool pass
false so the typed /skill goes through and the model loads the instructions
as a tool result, keeping the persisted transcript as what the user typed.
Workflows always expand — the tool does not serve them. isSkillsToolAvailable
exposes the catalog check hosts use to decide (yolo preset and the skills
tool toggle leave textual expansion as the only delivery path).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): skill slash commands load via the skills tool instead of expanding

The TUI user-command wrap and buildUserInputMessage now keep a typed /skill
as-is when the session's mode/toggles register the skills tool, matching the
desktop app; workflows keep expanding, and yolo (zen) keeps expanding skills
because its preset has no skills tool. This also fixes CLI resume/history
surfaces showing the skill body: the persisted user message is now the typed
command.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): keep configured skill slash commands typed for the skills tool

expandSlashCommands no longer splices a configured skill's instructions into
the model text; the SDK session's skills tool delivers them as a tool result
(previously they arrived twice). Builtin pseudo-skills like /deep-planning
are not served by that tool and keep expanding, as do workflows.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): use the shared skill-expansion option in the sidecar

Replaces the sidecar's workflow-detection dance with core's
expandSkillCommands option and gates on isSkillsToolAvailable, restoring
textual expansion where the tool is missing (yolo mode or the skills tool
toggle) — a gap in the previous desktop-only change.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): drop the display inverter for expanded transcripts

Accepted trade-off to keep the change minimal: sessions recorded before
skills switched to the skills tool, workflow sends (deprecated), and
yolo-mode skill sends persist expanded instructions and now render that text
as-is instead of being collapsed back to the typed /command at projection
time.

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>

* Use fixed selection chevron in account dialog to match other dialogs (#13364)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): align system prompt with session mode (#13361)

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): finalize queued turns on chat_done with canonical history reconcile (#13330)

Turns that settle through the event stream (queued prompts, including the
first prompt of a fresh session) resolve their send() RPC early, so nothing
cleared the streaming shimmer or reconciled live-streamed content against
the persisted transcript at turn end. A turn whose deltas were incomplete
stayed visually streaming forever and only healed when a later non-queued
send rehydrated history.

chat_done (and chat_session_ended / the queue-drain double check) now clears
the active assistant streaming id and schedules a short-delayed
read_session_messages + applyCanonicalHistory, guarded by turn epoch,
session id, and in-flight send submissions so it never clobbers a newer
turn or duplicates the blocking send path's own finalization.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.14

* fix(clients): filter non-chat models from chat pickers (#13317)

* fix(clients): filter non-chat models from chat pickers

* fix(clients): align chat model eligibility

* fix(desktop): strip user_input envelope when copying a user message (#13369)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>

* docs: show DeepSeek V4 peak and off-peak pricing (#13312)

* docs: update DeepSeek V4 average pricing

* docs: show DeepSeek peak and off-peak pricing

* docs: add GLM-5.3 reference pricing (same as GLM-5.2)

* docs: add GLM-5.3 to ClinePass models table

* fix(llms): display billed gateway cost (#13385)

* fix(shared): run PowerShell commands with fail-fast error semantics (#13358)

* fix(shared): run PowerShell commands with fail-fast error semantics

The run_commands PowerShell wrapper never set $ErrorActionPreference, so
the default 'Continue' applied: a pipeline erroring per item (e.g. a
malformed Where-Object over Get-ChildItem -Recurse) emitted one error
record per enumerated file - tens of thousands of stderr records on
large trees, looking like a hang - and could still resolve as SUCCESS
with exit 0.

Prepend $ErrorActionPreference='Stop'; to the script content executed
by the ScriptBlock so the first error terminates the command with a
non-zero exit and a single error message. Concatenated on the same line
as the user command so error line numbers stay unshifted.

Fixes #13285

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(shared): set the fail-fast preference in the bootstrap scope

Setting $ErrorActionPreference='Stop' by string-prepending it into the
scriptblock source displaced a leading param(...) from its mandatory
first-statement position, so scripts beginning with a param block failed
with CommandNotFoundException. Preference variables are dynamically
scoped, so setting Stop in the -Command bootstrap gives the invoked
scriptblock identical fail-fast semantics while keeping the user script
byte-identical (param works, error positions unshifted) and drops the
doubled-quote escaping.

* docs(shared): document the fail-fast tradeoffs in the PowerShell wrapper

Stop promotes every non-terminating error, not only per-item pipeline
floods: partial-result commands (recursive listings over access-denied
junctions) now stop at their first error, and Windows PowerShell 5.1
turns in-script stderr redirection of succeeding native commands fatal.
State this in the wrapper comment as a deliberate tradeoff, with the
GitHub Actions precedent and the per-command opt-outs.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* ci: stop over-long changelogs from silently dropping release Slack posts (#12955)

Slack section blocks reject text longer than 3000 characters. The Slack
action logs that rejection as ##[error] but does not fail the step, so an
over-long changelog drops the release announcement while the run stays
green — cline@3.0.50 (3272 chars) published to npm, tagged, and cut a
GitHub release with no Slack post and nothing red to notice.

Every publish workflow pasted the changelog section verbatim into one
section block, so all six were exposed; the SDK, desktop, and extension
sections were only 150-350 chars under the ceiling.

Add a slack_content output alongside content: unchanged when the section
fits, otherwise trimmed on a line boundary with a link to the full
release notes. Only the Slack payload uses it — GitHub release bodies and
the desktop updater manifest still get the whole section.

* ci: tidy workflow cache config and job permissions (#13403)

Publish workflows now always do clean npm installs (no dependency
cache in their test gates), the e2e workflow's cache keys are
exact-match only, and the e2e job drops an id-token permission it
never used.

* Rename desktop app from "Cline Code" to "Cline" (#13401)

* Rename desktop app from Cline Code to Cline

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Format touched Rust test assertions

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): surface provider-executed tool activity as observational events (#13300)

* fix(llms): surface provider-executed tool activity as observational events

Provider-executed tool parts (e.g. every tool the Claude Code CLI runs
inside its own session) were dropped by the model-tool guard added for
web search: only declared model tools were re-emitted, everything else
hit continue with nothing yielded. Those sessions modified the workspace
with no tool activity in runtime events, transcripts, or the UI.

Route all providerExecuted parts onto the observational path instead:
emit execution-tagged tool-call-delta and tool-result events, matched by
tool-call ID for providers that omit the flag on the result half. They
stay out of AgentRuntime's execution/approval loop, and the runtime
already persists them as modelToolActivities and projects them for
display.

The AgentModelEvent tool-result variant widens toolName from
ModelToolName to string to carry the provider's own tool names.

* fix(agents): keep turns that are only provider-executed tool activity

A turn consisting solely of observational tool activity has an empty
assistant content array - the activity lives in message metadata, since
projecting it into content would replay tool_use blocks the model never
gets results for. The empty-content guard threw on such turns, erroring
the run and losing the activity from the transcript. Count model-tool
activity as content for the emptiness check (error finishes still
throw); replay stays safe through the codec's empty-content placeholder.
Also drop the trailing text delta from one gateway test so the tool-only
stream shape stays covered end to end.

* feat: allow agents to create scheduled tasks (#13331)

* feat(core, desktop): add durable todo agenda

* fix(desktop): secure todo approvals and track tool usage

* fix(desktop): clean up failed approval delivery

* fix(desktop): authenticate approval connections

* fix(desktop): cancel approvals on broadcast failure

* fix(desktop): authenticate development approvals

* fix(desktop): harden development approvals

* test(core): make task paths cross-platform

* fix(desktop): serialize approval readiness

* refactor(core): unify todo and schedule tools

* feat(core): distinguish user todos from agent suggestions

* fix(core): hide tasks tool in yolo mode

* fix(core): enforce schedule workspace scope

* fix(core): bind schedule scope to hub connection

* fix(core): establish task scope at hub startup

* fix(core): scope task automation by workspace

* test(core): normalize workspace path expectations

* test(core): serialize Windows CI workers

* fix(core): reject unregistered schedule authority

* fix(desktop): guard task execution commands

* fix(core): avoid polynomial regex in mention parsing

* fix(core): address schedule tool review feedback

* fix(core): bind websocket clients to hub workspace

* fix(core): flatten tasks tool input schema

* fix(core): authorize multi-workspace hub clients

* test(core): type hub transport authority mock

* fix(cli): register a workspace client for remote schedule commands (#13398)

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): treat ClinePass as OAuth-managed in the chat credential gate (#13404)

* fix(desktop): treat ClinePass as OAuth-managed in chat credential gate

ClinePass shares the Cline account OAuth credentials (its auth handler
stores under the "cline" provider), so the webview never sees a plain
API key for it. The chat pre-flight check only exempted cline/oca/
openai-codex, so switching to ClinePass while signed in via OAuth
blocked with "Missing API key" even though the sidecar resolves the
stored access token fine (which is why the CLI worked).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: format helpers.test.ts with biome

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): stack code block lines when streamdown lineNumbers is off (#13412)

streamdown renders each Shiki token line as a bare inline span with no
newline text between non-empty lines, and only applies its block line
class when lineNumbers is on. With lineNumbers off (the desktop app's
config) every multi-line fenced block collapsed into one run-on line.
Make the direct line spans under code-block-body display: block in the
shared markdown.css; empty lines keep their height via their lone "\n"
child under white-space: pre.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): work summary undercounts wall time when pre-tool thinking attaches to the answer (#13413)

* fix(desktop): anchor work summary duration on the answer row, not attached pre-tool reasoning

The collapsed 'Worked for Xs' row undercounted wall time whenever a turn's
assistant message contained thinking + tool_use with no narration text: the
canonical projection emitted the reasoning-only row after the tool row (both
stamped before the tool executed), the webview attached that row to the final
answer, and collapseCompletedWork used the answer's earliest attached
reasoning timestamp as the end anchor - excluding the entire tool execution
(e.g. 'Worked for 5s' for a turn with an 8s command).

- webview: end the work span at the answer row's own timestamp, clamped to
  the last collapsed row so a fallback answer bubble with a synthetic early
  timestamp cannot shrink the duration either
- sidecar: flush pending thinking before a tool_use row so rehydrated
  transcripts keep the live-stream order (thinking before its tool call) and
  pre-tool reasoning no longer rides on the next answer

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep interleaved thinking between the tool calls it separates

Address Greptile review: when one assistant message interleaves thinking
between multiple tool_use blocks, each reasoning segment now projects at its
own position (attached to a text row from its own segment when present,
otherwise as its own row) instead of merging into the first reasoning row,
which displayed later thinking before a tool call it actually followed.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): remove settings gear hover state while Account screen is open (#13408)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't show "No sessions found" while session history is still loading (#13414)

* fix(desktop): don't show 'No sessions found' while session history is still loading

Replace the isLoadingHistory flag with hasLoadedHistory, set only once the
backend has actually answered a list_discovered_sessions request. The sidebar
and Sessions view now keep their loading state until that first definitive
response, so the empty-state copy can no longer appear while history is still
being fetched (or while a failed fetch is being retried).

Also retry a failed initial fetch on the 2s event cadence instead of stranding
the UI until the 12s periodic poll, which is what stretched the misleading
empty state to ~10 seconds after a webview reload when the websocket lost the
race with the page load.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop history fast-retry from re-arming after hook unmount

A failed initial fetch that settles after the hook unmounted could schedule a
new retry timer after cleanup had already cleared the refs, leaving the
abandoned hook polling the backend every 2s. Guard scheduleRefresh with a
disposed ref set by the mount effect's cleanup.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix @ file mentions breaking on paths with spaces (#13391)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with "command not found" on VS Code 1.134 (#13402)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with 'command not found' on VS Code 1.134

Code action commands carried arguments (expandedRange, diagnostics),
which routes them through VS Code's CommandsConverter cache. VS Code
1.134 disposes the cached entries before the clicked action executes,
so every lightbulb action failed with 'Actual command not found,
wanted to execute cline.addToChat'.

Drop the arguments so the command id is passed through directly, and
recover the context in the handler instead: getContextForCommand now
expands an empty selection by 3 surrounding lines (matching the old
provider behavior) and gathers document diagnostics intersecting the
range when none are passed explicitly.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Scope gathered diagnostics to the selection/cursor

Match the old CodeActionContext.diagnostics behavior: only include
diagnostics intersecting the range the action was requested for, not
the surrounding lines the text gets expanded to.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop: unify Plugins, MCP, and Skills into one Plugins hub with a dedicated Marketplace page (#13411)

* Unify desktop plugins, apps, MCP, and skills into one Plugins hub with a Browse directory mode

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Open the marketplace directory as a modal over the Plugins hub instead of swapping the page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename directory to Marketplace: Browse Marketplace button, Marketplace modal title with icon, search placeholder

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix search input focus ring clipped by the Marketplace modal scroll container

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Address Greptile review: keep selected tag chip visible when its count drops to zero, and remount installed tab when a marketplace install completes after the modal closed

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Track marketplace modal mutation flag in a ref so a close click racing a queued render cannot skip the inventory remount

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make Marketplace its own settings page under Customizations and restore Channels as a standalone page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Remove icon from Marketplace page header for consistency with other settings pages

* Notify mounted inventory views when the marketplace invalidates the cache so late install completions refresh the Plugins hub

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop/ui): recommended and free model tiers in the composer model selector (#13410)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK (#13415)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): recommended-feed badges and descriptions in provider settings (#13416)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* feat(desktop): recommended-feed badges and descriptions in provider settings

Review suggestion on #13410: the provider settings page has room for
more model detail than the composer's picker. The cline/cline-pass
provider cards now refresh their model list through
list_provider_models (the catalog snapshot deliberately skips the
recommended-feed overlay so the startup catalog fetch never blocks on
the feed) and render Recommended/Free tier badges plus feed tags (NEW)
next to the model name, with the model description underneath. The
refreshed list also surfaces the live entries instead of the bundled
snapshot.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): scope the settings featured model list to its provider and revision

The fetched featured list was unscoped component state: switching
between cline and cline-pass reused the component instance, so the
previous provider's models stayed visible while the new request was
pending (or forever, when it failed), and the retained copy shadowed
later provider.modelList updates — adding a second custom model
submitted the stale list as the complete configuration and dropped the
first addition.

The fetched list now only applies to the provider and modelList
revision it was fetched for (falling back to the catalog snapshot
otherwise and refetching on membership changes), and add-model submits
the union of the displayed and configured ids so an update can never
silently unconfigure existing entries.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): update packed-Tailwind smoke contract for the picker's max-h-64 (#13421)

The ui-publish smoke check pins a set of Tailwind candidates the packed
sources must emit; #13410 grew the SearchCombobox options list from
max-h-56 to max-h-64, so the publish run failed on the stale candidate.
All other pinned candidates verified against the current sources.

* feat(desktop): refresh app icons and branding (#13400)

* ci(vscode): upload E2E failure recordings from the right path (#13427)

The job sets working-directory: apps/vscode, but that default applies to run
steps only, not to `uses:` steps. Since #10961 moved the extension under apps/
and added that default, the artifact path has resolved against the repo root,
matched nothing, and every failing run logged "No files were found with the
provided path: test-results/playwright/" instead of uploading recordings.

Widen to test-results/ so Playwright's error-context snapshots ship alongside
the videos.

* fix(hooks): deliver tool hook contextModification to the model (#13297)

* fix(hooks): deliver tool hook contextModification to the model

On the next engine, a tool_call (PreToolUse) hook's contextModification
was parsed into HookControl.context and then silently dropped: the
runtime beforeTool/afterTool result contract had no channel for
injecting conversation context. Legacy consumed it (ToolExecutor /
ToolHookUtils pushed <hook_context> blocks into the next user turn), so
this was a regression of documented behavior.

- Add appendContext to AgentBeforeToolResult/AgentAfterToolResult.
- AgentRuntime collects appendContext across hooks during an
  iteration's tool executions and appends one <hook_context> user
  message after the tool results, keeping tool-result parts contiguous.
- Map HookControl.context into appendContext in both subprocess hook
  layers (skipped when the hook cancels, matching legacy, where the
  message doubled as the error).
- Truncate injected context at 50KB per hook output, matching legacy.
- Concatenate appendContext across merged hook layers.

tool_result (PostToolUse) hooks still run detached with stdout ignored;
making them blocking so their context can be collected is a follow-up.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): stamp tool identity on injected hook context blocks

Contexts are batched into one message after the tool results, and
parallel tool execution collects them in completion order, so position
alone cannot attribute a block to its tool call. Add tool_name and
tool_call_id attributes to each <hook_context> block.

* fix(hooks): sanitize hook context block markup

Attribute values (tool_name, tool_call_id) are stripped of quote/angle
characters and embedded </hook_context> closers in hook output are
neutralized, so neither provider-supplied ids nor hook text can corrupt
or spoof a block's stamped identity.

* fix(hooks): neutralize forged opening hook_context tags in hook output

The previous sanitization only neutralized closing tags, so hook output
could still open a forged <hook_context> block claiming another tool's
identity. Escape both opening and closing embedded tags with one rule.

* fix(hooks): hide injected hook context from user-facing transcripts

Stamp the injected hook-context user message with displayRole 'system'
(the compaction-summary convention) so it reaches the model but does
not render as a user bubble in live or replayed transcripts. Without
this, resuming a session showed the raw <hook_context> block as if the
user had typed it.

* fix(hooks): neutralize case-variant embedded hook_context tags

The tag-neutralization regex was case-sensitive, so hook output could
still smuggle a forged tag as <HOOK_CONTEXT>. Match case-insensitively.

* fix(vscode): map PreToolUse contextModification into runtime appendContext

The extension's hooks adapter bridged file hooks into the SDK runtime
but forwarded only cancel/errorMessage, so a PreToolUse hook's
contextModification never reached the model. Map it into the runtime's
appendContext channel; HookFactory already truncates it at 50KB.

* fix(vscode): hide hook-injected context from replayed transcripts

Live sessions never rendered the injected <hook_context> user message,
but session reload replayed it as a user bubble (and post-resume turns
kept doing so). Treat these messages as synthetic in the user-message
mapping: honor the displayRole 'system' stamp the runtime sets, with a
text-prefix guard for paths where metadata is unavailable. This also
keeps edit/regenerate ordinal mapping aligned with visible bubbles.

* fix(hooks): run file hooks through exactly one layer per host

The VS Code extension registered two independent hook execution layers:
its own hooks adapter (config.hooks) and the SDK core's file-hook
extension from the runtime bootstrap. When both discover the same hook
files, every hook executes twice per event — and with context injection
wired, each contextModification would be injected twice.

Add a 'hooks' runtime config extension kind (in the default set, so the
CLI keeps core file hooks unchanged) and gate the bootstrap's file-hook
extension on it. The extension excludes 'hooks' at session start, so
its adapter — which also provides the hook status UI and the
hooksEnabled setting — is its single execution path.

* fix(vscode): discover hooks from the session workspace, not only global state

Hook discovery read workspaceRoots from global state shared across
every Cline instance, so another window repointing it made workspace
hooks silently stop being discovered. With the extension's adapter now
the single hook execution layer, that meant no hooks at all.

HookFactory takes an optional sessionWorkspaceRoot and unions that
root's .clinerules/hooks into discovery (and into cwd resolution), fed
from the session config's cwd. Shared-state discovery still works, so
behavior in the single-window case is unchanged.

* fix(hooks): keep sanitized hook attribute values distinguishable

Replacing every markup delimiter with the same underscore could
collapse two tool call ids that differ only by such a character into
identical stamps. Escape each delimiter with a distinct token instead.

* fix(hooks): make hook attribute sanitization injective

Escaping the underscore itself turns the attribute escaping into a
uniquely decodable code, so no two distinct tool call ids can collapse
to the same sanitized stamp (previously an id containing a literal
escape token could collide with an id containing the delimiter).

* fix(vscode): reconstruct hook status rows when replaying transcripts

hook_status messages are emitted live but never persisted, so reloading
a session dropped every hook row. The injected <hook_context> blocks
carry the hook source and tool name, so the replay translator now
rebuilds a completed hook status row from each block. The injection is
also no longer treated as a user turn boundary, so the final turn's
completion retag is unaffected by it.

* fix(hooks): collect PostToolUse hook output and honor its control (#13298)

* fix(hooks): collect PostToolUse hook output and honor its control

tool_result (PostToolUse) hooks ran fire-and-forget with stdout
ignored, so their entire JSON output — contextModification and cancel —
was discarded. Legacy awaited PostToolUse, injected its
contextModification into the conversation, and honored cancel.

- Run tool_result hook commands blocking (same 120s default timeout as
  tool_call) in both the hook-config-file layer and the agent-hook
  subprocess layer.
- Map their output: cancel stops the run with the hook's error message
  as the reason; otherwise context is injected via afterTool
  appendContext.

This restores legacy blocking semantics: tool results now wait for
tool_result hooks, but only in sessions that have one configured.

Ref: https://linear.app/cline-bot/issue/CLINE-2987

* fix(hooks): bound tool_result hook wait and isolate cancel reason

Address review findings:
- The agent-hook subprocess layer forwarded an unset timeoutMs
  unchanged, so a tool hook command that never exits would block the
  agent indefinitely. Default both tool_call and tool_result to the
  120s bound the hook-config-file layer already used.
- A cancelling hook's error message was folded into the same context
  field as other hooks' injectable context, so merging controls could
  leak unrelated hook context into the cancellation reason. Carry it as
  a separate cancelReason, and surface it as the stop reason for
  beforeTool cancels too.

* fix(hooks): prefer errorMessage as a cancelling hook's stop reason

When a cancelling hook returns both contextModification and
errorMessage, the context-first parse precedence made the injectable
context the cancel reason and discarded the actual error. Parse the two
fields separately: errorMessage wins as the cancel reason (matching
legacy), and a lone errorMessage still folds into injectable context
for non-cancelling hooks as before.

* fix(vscode): honor PostToolUse hook cancel and contextModification

The adapter awaited PostToolUse hooks but discarded their output
entirely. Map cancel to a stop control (with errorMessage as the
reason) and contextModification into the runtime appendContext channel,
matching the PreToolUse mapping and legacy semantics.

* fix(hooks): whitespace-only errorMessage no longer suppresses the cancel reason

A cancelling hook returning meaningful context alongside a blank
errorMessage lost both: the parsers selected the whitespace as the
reason and the result mappers trimmed it away. Require a non-blank
errorMessage before it wins, so context serves as the fallback reason.
Apply the same fallback in the extension adapter's stop mapping.

* fix(core): stop Windows CI worker crashes from the agenda spec watcher (#13428)

* fix(core): watch agenda task specs via the resolved long path

fs.watch on a path with 8.3 short components (e.g. C:\Users\RUNNER~1
temp dirs) trips a libuv assertion in fs-event.c on Windows and aborts
the whole process. Since the agenda task manager landed, every hub
server test spins up its spec watcher on such a path on hosted Windows
runners, killing the vitest worker and failing the sdk-test Windows job
on every branch. Resolve the specs dir with realpathSync.native before
watching so libuv only ever sees the long form.

* test(ui): stub ResizeObserver for @pierre/diffs in tool-diff tests

jsdom does not implement ResizeObserver, so every ToolFileDiff render
logged a ReferenceError from @pierre/diffs to stderr. Tests still
passed; this just silences the noise the same way the constructable
stylesheet shim does.

* fix(core): skip the agenda spec watcher when the dir does not resolve

Falling back to the raw path on realpath failure would reintroduce the
Windows short-path abort; log and go without the watcher instead.

* fix(vscode): honor the classic truncation range when migrating legacy tasks (#13419)

Classic Cline truncated long conversations by omitting an index range of
api_conversation_history from every API request (keep the first
user-assistant pair, drop everything through the range end, strip
orphaned tool_results from the first kept message). The range was
persisted on the history item while the full history stayed on disk.

legacyApiHistoryToSdkMessages ignored conversationHistoryDeletedRange
and converted the entire file, so resuming a migrated long task handed
the SDK an untruncated working context that could exceed the model's
context window by millions of tokens - every request failed with
'prompt is too long' and every compaction restarted from the full
history (#12996, confirmed by the reporter: the task was migrated from
an older version and broke after a restart, with each compaction
starting from ~3M tokens).

The migration now replays exactly what the classic extension sent:
slice out the deleted range and drop orphaned tool_results, mirroring
ContextManager.getTruncatedMessages (see origin/main). Malformed ranges
fall back to the full history (previous behavior).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): show the diff edit view for multi-line edits in CRLF files (#13417)

The edit preview computed proposed content with an exact old_text match, but
the SDK executor normalizes old/new text to the file's own line endings before
matching (#12305) - reads strip CR, so models emit LF-only text even for CRLF
files. Any multi-line old_text in a CRLF file therefore failed the preview's
match: the diff edit view silently never opened while the executor applied the
edit. Single-line edits (no line break in old_text) were unaffected, which is
why the diff view appeared to trigger inconsistently.

Mirror the executor's EOL normalization (and its literal $-sequence insertion)
in the preview computation.

Fixes #13296

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): report truthful session status so desktop checkpoint restore stops wedging (#13418)

* fix(core): keep hub session status truthful across queue-drained turns

Queue-drained turns settle only through the event stream, but the hub
runtime host mistranslated their lifecycle in two ways:

- session.updated events carrying only a snapshot (persistence updates)
  defaulted the projected status to "running". When one trailed the
  final idle update after a turn, clients that track busy state from
  status events (the desktop sidecar's workspace restore gate) stayed
  busy forever. Use the snapshot's real status and emit nothing when
  neither source reports one.
- the per-run agent.done dedup was only reset by run.started, which the
  daemon-side queue drain never publishes, so a drained turn's done was
  swallowed as a duplicate of the previous turn's. Reset the dedup on
  session.pending_prompt_submitted, and suppress stale run.completed
  events that land inside a drained turn's window so they can neither
  emit a phantom done nor consume the drained turn's dedup slot.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* test(desktop): cover restore unlock after an event-settled queued turn

Exports the sidecar's core-session event handler so the queued-turn
lifecycle (busy via status events, cleared by the done agent event,
restore allowed afterwards) is testable end-to-end.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): start interactive sessions without a prompt as idle

The runtime host reported every new session as "running" until its
first turn ended. Interactive hosts (the desktop app) start sessions
with no prompt and dispatch turns through separate send calls, so a
created-but-never-prompted session stayed "running" forever — wedging
clients that gate workspace operations (checkpoint restore, message
edit) on active turns.

Interactive no-prompt starts now begin idle, start emits the session's
actual status (resumed sessions no longer masquerade as running), and
markTurn* transitions keep tracking in-memory status for lazily
persisted sessions so the first turn still reports running -> idle.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: format hub-runtime-host test filter

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor: drop the drained-turn done bookkeeping, keep the minimal fix

The stuck restore is fully explained by the two status defects (fabricated
"running" from snapshot-only session.updated events, and never-prompted
interactive sessions reporting "running"). The done-dedup machinery for
queue-drained turns addressed a separate cosmetic gap (queued turns emit no
chat_done, pre-existing) and required fragile run-window heuristics, so it
is removed to keep this change reviewable. Sidecar test now settles the
queued turn through the status event, matching the shipped mechanism.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs(sdk): document the truthful session-status contract

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(deps): update Langfuse packages and bump app versions (#13443)

* chore(deps): update Langfuse packages and bump app versions

Update @langfuse/otel to v5.10.1 and add @langfuse/vercel-ai-sdk v5.9.1 for improved observability with Vercel AI SDK.

Bump versions for @cline/code to 0.0.14 and @cline/ui to 0.2.0-next.6, updated via bun.lock.

Other Changes:
Added optional userId to AgentRuntimeConfig.
Propagated userId, sessionId, conversationId, runId, iteration, provider, and model context into AI SDK telemetry.
Added AI SDK 7 runtimeContext with explicit includeRuntimeContext.
Added stable OTEL_SERVICE_NAME=cline-sdk.
Added runtime metadata assertions in agent tests.

* add taskId

* Revert "add taskId"

This reverts commit f20d31d96d.

* docs: simplify Open Cline step in installing guide (#13405)

* docs: remove duplicate GLM-5.3 rows in ClinePass tables (#13449)

Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>

* chore(sdk): release v0.0.76

* chore(cli): release v3.0.56

* docs(cli): scope the v3.0.56 release notes to CLI-visible changes

* feat(desktop): interactive welcome hero graphic (#13399)

* feat(desktop): add interactive welcome hero

* feat(desktop): support composable welcome hero variants

* feat(desktop): reskin first-run onboarding (#13441)

* refactor: centralize client tool availability (#13451)

* chore(sdk): release v0.0.77

* docs(cli): drop the tasks tool from the v3.0.56 notes, it is desktop-only

* chore(vscode): prepare 4.1.11 release

* chore(desktop): release v0.0.15

* fix(llms): restore pinned media models atop the regenerated catalog

The desktop-experimental media/voice models were hand-pinned into
catalog.generated.ts (17e8c0cbc9) rather than produced by the generator,
so taking main's regeneration dropped every audio/video/TTS/realtime
entry the beta voice features select from. Re-add the 105 pinned media
models on top of main's fresh snapshot. Follow-up: move the pinned set
into a generator overlay so regeneration stops erasing it.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: JasmineLCY <38378321+JasmineLCY@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Haley Park <haleypark.design@gmail.com>
Co-authored-by: cline-cloud[bot] <276134852+cline-cloud[bot]@users.noreply.github.com>
2026-08-21 11:59:27 -07:00
Bee 0065e946b6 fix(desktop): ui conflicts from merge (#13450)
* fix(desktop): ui conflicts from merge

* fix(desktop): address avatar review feedback
2026-08-20 22:49:18 -07:00
John Choi 664985e330 fix(desktop): preserve live handoff prompt (#13434)
* fix(desktop): preserve handoff prompt during hydration

* fix(desktop): bound handoff prompt replay

* fix(desktop): preserve queued prompt in stale snapshots

* fix(desktop): track interleaved prompt occurrences

* fix(desktop): harden handoff prompt replay

* refactor(desktop): isolate cloud prompt replay

* fix(desktop): preview handoff prompt during setup

* fix(desktop): preview handoff images during setup

* refactor(desktop): scope handoff preview to webview

* fix(desktop): reconcile handoff prompt preview

* refactor(desktop): simplify handoff prompt preview

* fix(desktop): reconcile handoff prompt without clocks

* fix(desktop): anchor handoff prompt ordering

* fix(desktop): ignore optimistic handoff matches
2026-08-20 22:27:15 -07:00
John Choi 5801d559ff fix(desktop): keep closed model popover from blocking composer input (#13431)
* fix(desktop): disable closed model popover interactions

* fix(desktop): collapse closed model popover
2026-08-20 12:34:13 -07:00
+2 2e2400ad1f chore(desktop): sync main and cut 0.0.15-beta.1 (#13425)
* fix(vscode): continue the surviving session on resume instead of rebuilding with the original task text (#13175)

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilt the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

The preserved conversation history is the source of truth on resume, so
the fallback prompt now just asks the model to reassess the history and
continue, matching the legacy resume prompt which also never resent the
original task. User-typed text still takes precedence when provided.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): continue the surviving idle session on follow-ups instead of rebuilding

Stopping a turn keeps the session alive, but every idle follow-up (bare
Resume after Stop, and typed follow-ups after a completed turn) tore
that session down and rebuilt it from persisted task history before
sending. Continue the matching idle session in place instead, the same
way the CLI reuses the live session after an abort. Rebuilding from
history now only happens when no live session matches the displayed
task (task opened from history, extension host reload).

A bare resume still needs a prompt to start a turn, so it sends the
neutral [TASK RESUMPTION] prompt (shared with the rebuild fallback and
hidden from the transcript); user-typed content is echoed and sent
as-is. If the send lands while the abort is still settling, the runtime
auto-queues it and drains once the abort completes.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): consolidate follow-up send paths in SdkFollowupCoordinator

Now that idle follow-ups continue the live session in place, the
two-mode sendToActiveSession helper was redundant: its non-queued branch
duplicated continueIdleSession minus the bare-resume prompt. Split it
into a single-purpose queueToActiveSession and fold the idle no-task
send into continueIdleSession, flattening askResponse's decision tree
to: queue onto a running turn, continue a matching live idle session,
rebuild from history, or abandon.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): reuse the existing neutral resumption prompt for bare resumes

Drop the newly invented long resumption wording in favor of the phrase
that already existed as the no-history fallback and that the transcript
hiding logic and test fixtures recognize: '[TASK RESUMPTION] Please
continue where you left off.' The net change to resumeSessionFromTask
against main is now just deleting the branch that resubmitted
historyItem.task as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilds the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

Bare resumes now always use the neutral prompt that already existed as
the no-history fallback; user-typed text still takes precedence. This
matches the legacy resume prompt (responses.taskResumption), which only
ever included user-supplied text as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): hide synthetic prompts from the queued-prompt echo

A send that races a settling abort is auto-queued by the runtime, so a
bare Resume can reach the pending_prompt_submitted echo carrying the
synthetic [TASK RESUMPTION] prompt. Echoing it leaked model-facing text
as a visible user bubble and shifted the visible-user-message ordinals
that edit/regenerate mapping relies on. Filter synthetic prompts with
isSyntheticUserPrompt, keeping user attachments visible (matching
isSyntheticSdkUserMessage semantics).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): preserve LiteLLM input token limits (#13293)

* fix(vscode): preserve LiteLLM input token limits

* fix(vscode): prefer live LiteLLM model metadata

* fix(vscode): generalize private catalog metadata

* test(vscode): preserve llms exports in vscode lm mock

* fix(vscode): point provider signup URLs at their API key pages (#13337)

* fix(vscode): point Mistral signup URL at the general API keys console

The Mistral provider's signup link led to the Codestral console, which
issues Codestral-scoped keys that fail with 401 on api.mistral.ai — the
endpoint the provider actually calls. Point it at the general API keys
page instead.

Fixes #13288

* fix(vscode): deep-link DeepSeek and Fireworks signup URLs to their API key pages

Both pointed at marketing homepages; link straight to the key-creation
pages instead, matching the rest of the registry and the desktop app's
provider-key-urls map.

* fix(ci): always build the legacy bundle from the legacy-extension branch (#13349)

The combined-VSIX workflow took legacy-ref as a free-form dispatch input
with no publish-time validation (next-ref has one: publish requires main).
Any typed ref — a PR merge ref, an unprotected branch — would be built
into the published VSIX by the environment-less build job, and the publish
environment approver only ever sees an opaque prebuilt artifact, so the
approval protected the marketplace PAT but not the shipped bytes.

Remove the input entirely and hardcode the protected legacy-extension
branch, which makes that branch's protection rules load-bearing for
releases. The tested-sha pinning between test-legacy and build is
unchanged. publish-extension skill dispatch command updated to match.

* fix(ci): lock the legacy publish workflow to the legacy-extension branch (#13350)

The branch dispatch input was a free-form string with no validation. Both
jobs checked it out and ran full npm lifecycle scripts from it: the publish
job next to VSCE_PAT/OVSX_PAT (and npm run publish:marketplace executes a
script from that same ref with the PATs in env), and the test job with NO
environment approval at all while inheriting the workflow-level
contents/packages/checks/pull-requests write grants. A dispatch pointing at
e.g. refs/pull/N/head would run outside-contributor code with the
marketplace keys behind one approval, or with a repo-write token behind
none.

Remove the input and hardcode the protected legacy-extension branch, drop
the workflow-level permissions to contents: read, and elevate only the
publish job to contents: write (tag push + GitHub release). The branch
input's default was legacy-extension, so normal publishes are unchanged.
publish-extension skill dispatch command updated to match.

* fix(vscode): SDK remote-config parity — refresh coordination, session gating, and fail-closed opt-out (#13226)

* feat(desktop): native notifications (#13166)

* feat(desktop): native notifications

* macos target

* fix(desktop): isolate macOS dev app identity

* fix(desktop): address notification review feedback

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched (#13310)

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched

Toggling an auto-approve setting while a task is open writes
autoApprovalSettings into the StateManager's task-settings overlay
(updateAutoApprovalSettings -> setTaskSettings). The SDK controller never
cleared that overlay on clearTask/showTaskWithId (the legacy controller
did), so after New Task the stale overlay kept shadowing global settings
in getGlobalSettingsKey(): toggle RPCs were accepted into global state,
but every posted state still carried the overlay's old version, which the
webview rejects as not newer - the auto-approve checkboxes froze forever.

Restore legacy parity in SdkTaskControlCoordinator: drop the overlay
(persisting pending writes first) in clearTask() and before installing a
different task's proxy in showTaskWithId().

Fixes #13260

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* changeset

* test(vscode): add end-to-end regression test for auto-approve freeze after New Task

Wires the real StateManager, the real updateAutoApprovalSettings handler,
and the real SdkTaskControlCoordinator.clearTask() together with the
webview's version gate modeled on ExtensionStateContext, pinning the
end-to-end invariant behind #13260: checkbox toggles must keep reaching
the webview after a mid-task toggle followed by New Task. Verified the
test fails when the clearTaskSettings() call is removed from clearTask().

* fix implicit any in regression test

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): show provider web-search support under the settings toggle (#13328)

* feat(desktop): show provider web-search support under the settings toggle

The global Web search toggle silently does nothing unless the session's
provider offers native web search, which made the setting read as if it
worked with any provider. The desktop General settings row now explains
that only providers with built-in web search honor it, and shows a live
status line: which connected providers are ready to use it (no extra
setup needed), or an amber warning with a link to the Models section
when none of them support it.

Support is resolved in the webview via a new providerOffersModelTool
helper in @cline/llms (browser export), sharing the same builtin-manifest
source of truth as the runtime's supportsModelTool attachment check.

* fix(desktop): address review — refetch web-search status on catalog invalidation, clarify per-model support

Greptile P2: the one-time catalog fetch could race an in-flight provider
save and show stale status; the row now refetches when the provider
catalog cache is invalidated (fired after saves complete).

Greptile P1: the ready line implied every model on the provider works;
Vertex excludes Claude routes, so the copy now scopes the promise to
models that support it.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* feat(ui/desktop): collapse finished runs into a work summary and remove hover-state dead space (#13315)

* feat(ui): add WorkActivity collapsed-run summary and float message actions as a pill

WorkActivity/WorkActivityTrigger/WorkActivityContent fold a finished agent
run's working rows (tool calls, thinking traces, narration) behind a single
"Worked for 4m 12s · 14 tool calls" disclosure built on the shared animated
disclosure primitives, with formatWorkActivityLabel/formatWorkDuration
exported for consumers.

Message hover actions no longer rely on the transcript reserving blank space
below each message: the action row is now a self-backed pill (border,
blurred background, shadow) that floats over whatever follows, so
conversations can pack rows tightly without hover chrome colliding with the
next message.

* feat(desktop): collapse finished runs into a work summary and tighten chat spacing

collapseCompletedWork post-processes the grouped transcript: once a run ends
on assistant text with no further tool calls, its working rows fold into one
expandable WorkActivity row while the final answer stays visible. Runs are
delimited by user messages; the trailing run only collapses when the session
has stopped running and actually produced an answer, so live streams and
cancelled/failed tails keep their rows. Assistant messages carrying images
or media are treated as deliverables and never collapse.

The conversation list gap drops from gap-8 to gap-4 now that hover actions
are self-backed pills that need no reserved space, and user messages add
their own top margin so turn boundaries stay visually distinct.

* refactor(ui/desktop): work summary label wording, flat expansion, stable in-run rhythm

Feedback round on #13315:

- Label reads "Worked for 4m 12s and made 14 tool calls" instead of joining
  with a dot; without a duration it falls back to "Made N tool calls".
- Expanded work rows render at transcript level — no rail or extra indent —
  since tool rows and thinking traces already carry their own nesting when
  expanded. The work content keeps the tight working-row rhythm.
- Live working rows (thinking traces + tool calls) now group into a 'run'
  render item with the same tight 0.25rem rhythm, so there is no oversized
  gap under a "Thought for Ns" row and every row keeps its exact position
  when the finished run folds into the work summary. A trailing
  answer-in-progress stays outside the group at transcript level, and pure
  prose spans keep normal spacing.
- The transient "Thinking..." indicator moves inside the transcript column
  and mirrors a trigger row's geometry, so the first real row replaces it in
  place with no jump.

* style(ui/desktop): hover-pill metrics, right-pointing work chevron, scroll and spacing fixes

Another feedback round on #13315:

- Hover action pill: +2px internal padding, a trailing inset after the
  timestamp (it sat flush against the pill border), and more clearance
  between the message content and the pill (2px -> 6px; the hover bridge
  grows to match).
- The work summary chevron points right while collapsed and continues
  counterclockwise to point up when expanded.
- Conversation bottom padding drops pb-20 -> pb-8: the composer sits below
  the scroller, so the padding only needs to clear a pinned action pill.
- Sending a message scrolls back to the bottom even if the reader had
  scrolled up (new AutoScrollOnSend on the user-message count, which ignores
  optimistic-bubble re-keying; @cline/ui now exports useConversation for
  this).
- An assistant answer directly under its run's working rows pulls itself
  0.5rem closer than the full transcript gap.

* style(desktop): leave a visible gap between a pinned action pill and the composer

pb-8 exactly matched the pill's ~40px footprint, so the last row's hover
actions sat flush against the composer top; pb-12 restores ~8px of daylight.

* style(desktop): widen the gap between the pinned action pill and the composer to ~24px

pb-12 left only ~8px of daylight under the pill; pb-16 reads comfortable
without reverting to pb-20's dead space.

* fix(desktop): keep the thinking indicator at the working-row offset mid-run

The indicator matched a trigger row's geometry but sat a full transcript gap
(1rem) below the last working row, while the tool/thinking row replacing it
joins the tight run group at 0.25rem — a visible upward jump. When the last
transcript item is working rows (or streamed assistant output), the
indicator now pulls up to the same tight offset; only at the start of a run,
under the user message, does it keep the normal gap.

* style(ui): calm the hover actions surface per team feedback

Borderless rectangle instead of the bordered pill: radius drops to
var(--radius), the side padding goes entirely (the icon buttons carry their
own hit areas), and the vertical padding halves. Blurred background and
shadow stay so it remains legible over following content.

* feat(ui/desktop): full-band hover reveal and iOS-style disclosure easing

The hover actions only appeared while the pointer was inside the message
box itself. The invisible bridge under each message now spans the full
height of the band the floating actions occupy (full row width), so
hovering anywhere in that strip reveals them. Sibling row types
(.cline-chat-tool, .cline-chat-work, and the desktop's run/tool groups)
become position: relative so they paint above the bridge — their own
content keeps its hover and clicks, and the bridge only wins in the band's
genuinely empty space.

All expandable rows (work summary, tool panels, thinking) open and close on
a 240ms symmetric ease-in-out cubic-bezier instead of the 60ms snap, with
chevron rotation on the same curve. Reduced-motion still disables both.

* revert(ui/desktop): drop the full-band hover reveal; quicken disclosure easing to 180ms

The full-band hover bridge (and the position: relative changes that made it
safe) is reverted per feedback — back to the narrow bridge that only spans
the gap under the message. The iOS-style ease-in-out on disclosures stays
but speeds up from 240ms to 180ms.

* fix(ui): recover live tool diffs that mount as a blank pierre skeleton

Live-streamed edit rows could show an empty diff for the whole run, with the
diff only appearing after the collapsed work row was expanded (fresh mount).
Root cause, confirmed by driving a live session and inspecting the element:
React StrictMode double-invokes @pierre/diffs' ref callback; the first
instance's async highlight work aborts on its immediate cleanup, and the
second instance adopts the abandoned half-rendered shadow tree as if it were
complete prerendered output — zero height, no code, no theme stylesheet,
permanently. A rendered diff always carries style[data-theme-css] in its
shadow root, so ToolFileDiff now checks for it shortly after mount and
remounts FileDiff (bounded attempts) when missing; the fresh host element
takes the normal render path and recovers within ~400ms. Verified live: the
diff now renders during the run.

* fix(desktop): keep interrupted runs expanded even with partial trailing text

The trailing-run collapse gated on 'ended with assistant text', which
misread a Stop that landed mid-answer as a finished run and folded the tool
calls the user wants to inspect. The gate is now the terminal status itself:
only completed (or restored-idle) sessions collapse the trailing run;
cancelled/failed/error tails keep their rows regardless of partial text.
(Greptile P1 on #13315 — matches the PR's stated rule.)

* feat(ui): share the markdown pipeline, chat polish, and ThinkingBlock across products (#13323)

* feat(ui): share the markdown pipeline, chat polish CSS, and ThinkingBlock

The desktop app and the cloud dashboard both consume @cline/ui yet rendered
assistant output differently, because Markdown policy and the thinking-trace
row lived app-side. This moves the shareable parts into the package:

- components/markdown (new export): the lazy Shiki code highlighter (GitHub
  light/dark, pinned language set) and agentMarkdownControls — the standard
  Streamdown configuration. streamdown/shiki/@shikijs/* become optional peer
  dependencies, mirroring @pierre/diffs.
- components/markdown.css: the desktop's chat polish moves in — chat-scale
  headings, outside list markers, single quiet code blocks with a
  hover-revealed copy control, table cards. Kept unlayered so it beats
  Streamdown's layered Tailwind utilities without !important.
- ThinkingBlock + formatThoughtLabel in agent-chat: the standard thinking
  row (brain icon, Thinking/Thought-for-Ns label, streaming shimmer, rail
  presentation, capped scrollable body). The shimmer and the
  reasoning-hover-suppression rule move into agent-chat.css; triggers gain
  the color transition the desktop applied locally.

Version bumps to 0.2.0-next.5 for the dashboard to pick up.

* refactor(desktop): consume shared markdown and thinking primitives from @cline/ui

The local Shiki highlighter, Streamdown controls, chat markdown polish CSS,
streaming-title shimmer, and reasoning hover-suppression rule are deleted in
favor of the @cline/ui versions (the highlighter test moves to the package's
suite). ReasoningBlock becomes a thin wrapper that hands MemoizedMarkdown to
the shared ThinkingBlock, and formatThoughtLabel re-exports from the package
so grouping code and tests keep their import path.

globals.css now imports @cline/ui/components/markdown.css (unlayered, so the
polish keeps beating Streamdown's layered utilities); the app keeps only what
is genuinely app-specific: link/image policy in markdown.tsx, selectability
rules, accent palettes, and the view-enter transition.

* style(ui/desktop): make thinking-trace prose legible

Thinking body text rendered too faint: plain muted-foreground plus the
desktop's font-thin weight. The shared thinking content now leans 75% of the
way back toward the body text color (still slightly de-emphasized), and the
desktop drops the thin font weight.

* ci(ui-publish): build @cline/shared before ui typecheck (#13354)

@cline/ui's generated-media imports @cline/shared/browser, which resolves to
shared's dist output. The build-shared step sat after typecheck/test/build,
so the first ui-publish dispatch since #13025 failed at Typecheck UI with
TS2307. Move the step to right after install.

* fix: run_commands object form without args routes through the shell instead of failing with ENOENT (#13336)

* fix: run_commands object form without args routes through the shell

The structured { command, args? } form of run_commands was always spawned
directly with shell: false. When a model emitted a full command line in
command with no args (e.g. { command: "echo hello" }), spawn failed with
ENOENT for any command containing a space, breaking command execution for
the whole session.

Direct exec now only applies when a non-empty args list is provided; the
object form without args is routed through getShellInvocation like the
string form. Schema descriptions are tightened so models put arguments in
args instead of embedding them in command.

Fixes #13279

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: trim structured-command schema descriptions

The union schema is only used for lenient validation of input the model
already sent; its descriptions never reach a model prompt. Keep them
short instead of restating executor behavior.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: simplify direct-exec comment in shell executor

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert: keep original structured-command schema description

The description never reaches a model prompt and the executor now handles
both shapes, so the wording change was cosmetic noise.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: gate direct exec on args key presence, not array length

Review feedback: an explicit empty args array is intentionally structured
input and stays direct exec; only an object with no args key is treated
as a full shell command line. Matches the key-presence rule already used
by the VS Code host's formatCommandForTerminal. Also replaces the
empty-args shell test (which was PowerShell-incompatible) with a test
pinning the direct-exec contract.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: normalize Gemini custom base URLs for legacy host-root values (#13329)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs: add GLM-5.3 to ClinePass models and reference pricing (#13357)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): stream run command output (#13179)

* feat(desktop): stream run command output

* fix(sdk): clean up detached command logs

* fix(sdk): reap detached logs after hub restarts

* fix(sdk): preserve live detached command logs

* fix(desktop): harden live command progress

* fix(sdk): recover detached logs for local hosts

* fix(desktop): reconcile command output tool rows

* fix(sdk): retain logs for surviving commands

* fix(core): prevent PID reuse from retaining detached logs

* fix(core): preserve detached logs on probe failures

* fix(core): retain detached logs during probe outages

* fix(desktop): resolve leftover merge conflict in messages projection test

Combine both sides of the assertion: main's incremented per-block
createdAt projection and this branch's toolCallId/hookEventName meta.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): make TUI dialog colors follow theme changes live (#13355)

* fix(cli): make TUI dialog colors follow theme changes live

Dialog content previously read the static palette constant, so open
dialogs (including the theme picker itself) kept the default dark-blue
accents while scrolling through theme previews. Add getDialogPalette /
useDialogPalette, which resolve dialog colors from the active theme's
dialog accents and re-render on every theme change, and migrate all
dialog-rendered components to it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(cli): derive dialog panel background from the active theme

Dark themes now lift their own background one OKLAB step for the dialog
surface, so panels keep the theme's hue instead of the library's fixed
#262626. DialogThemeSync pushes the surface into the dialog container
for new dialogs and repaints open panels, so the surface also follows
live theme previews. Light themes keep the neutral dark panel to match
the dark accent fallback and the light-on-dark dialog text.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: skill slash commands load via the skills tool instead of expanding into the user message (#13327)

* fix(desktop): show typed slash command instead of expanded skill markdown

The sidecar expands /skill and /workflow tokens into their instructions
before dispatching, so the runtime's persisted transcript only contains the
expanded text. After a turn (and when reopening a session) the webview
re-hydrates from that history and rendered the whole SKILL.md body as the
user's message; queue events echoing the expanded prompt could also add a
second user bubble, and fresh sessions were titled with the markdown's first
line. The CLI never shows this because its TUI keeps the typed text in its
own transcript and only sends the expanded prompt to the model.

Mirror that separation inside the desktop sidecar's display boundaries:

- history projection (readSessionMessages) inverts user text that starts
  with a configured command's instructions back to '/name remainder',
  which also repairs sessions recorded before this fix
- queue snapshots and chat_queued_prompt_start events echo the typed
  prompt recorded at expansion time, so the webview's optimistic-bubble
  re-key matches again
- an untitled session sent an expanded prompt gets titled from the typed
  command instead of the instructions' first line

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't overwrite a mid-turn rename with the typed-command title

The untitled check ran before dispatch, so renaming a fresh slash-command
session while its first turn was running got clobbered by the post-turn
typed-command title. Re-check at write time and only replace a missing title
or the one the runtime auto-derived from the expanded prompt.

Also documents the inherent prefix-inversion ambiguity flagged in review:
text hand-typed with a command's exact instructions persists byte-identically
to that command's expansion, so stored history alone cannot distinguish them.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop expanding skill commands; let the skills tool load them

Pasting the skill body into the prompt is why the transcript could ever show
it: the desktop webview re-hydrates from the runtime's persisted history, so
whatever the sidecar splices into the user message renders as if the user
typed it. The runtime already registers the skills tool, whose description
requires the model to invoke it whenever the user references a slash command
— so send the typed /skill text through and let the tool deliver the
instructions as a tool result (previously they arrived twice: pasted and via
the tool). The persisted user message, session title, and queue entries are
then simply the typed command, which deletes the typed-prompt registry, the
queue event/snapshot rewriting, and the title machinery from the previous
approach.

Workflows are not served by the skills tool and keep textual expansion, so
the read-time display inverter stays: it collapses expanded workflow prompts
— and skill prompts persisted before this change — back to the typed
/command in the history projection.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): option to keep skill slash commands typed for the skills tool

resolveRuntimeSlashCommandFromWatcher (and the hub snapshot proxy) accept
expandSkillCommands: hosts whose sessions register the skills tool pass
false so the typed /skill goes through and the model loads the instructions
as a tool result, keeping the persisted transcript as what the user typed.
Workflows always expand — the tool does not serve them. isSkillsToolAvailable
exposes the catalog check hosts use to decide (yolo preset and the skills
tool toggle leave textual expansion as the only delivery path).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): skill slash commands load via the skills tool instead of expanding

The TUI user-command wrap and buildUserInputMessage now keep a typed /skill
as-is when the session's mode/toggles register the skills tool, matching the
desktop app; workflows keep expanding, and yolo (zen) keeps expanding skills
because its preset has no skills tool. This also fixes CLI resume/history
surfaces showing the skill body: the persisted user message is now the typed
command.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): keep configured skill slash commands typed for the skills tool

expandSlashCommands no longer splices a configured skill's instructions into
the model text; the SDK session's skills tool delivers them as a tool result
(previously they arrived twice). Builtin pseudo-skills like /deep-planning
are not served by that tool and keep expanding, as do workflows.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): use the shared skill-expansion option in the sidecar

Replaces the sidecar's workflow-detection dance with core's
expandSkillCommands option and gates on isSkillsToolAvailable, restoring
textual expansion where the tool is missing (yolo mode or the skills tool
toggle) — a gap in the previous desktop-only change.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): drop the display inverter for expanded transcripts

Accepted trade-off to keep the change minimal: sessions recorded before
skills switched to the skills tool, workflow sends (deprecated), and
yolo-mode skill sends persist expanded instructions and now render that text
as-is instead of being collapsed back to the typed /command at projection
time.

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>

* Use fixed selection chevron in account dialog to match other dialogs (#13364)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): align system prompt with session mode (#13361)

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): finalize queued turns on chat_done with canonical history reconcile (#13330)

Turns that settle through the event stream (queued prompts, including the
first prompt of a fresh session) resolve their send() RPC early, so nothing
cleared the streaming shimmer or reconciled live-streamed content against
the persisted transcript at turn end. A turn whose deltas were incomplete
stayed visually streaming forever and only healed when a later non-queued
send rehydrated history.

chat_done (and chat_session_ended / the queue-drain double check) now clears
the active assistant streaming id and schedules a short-delayed
read_session_messages + applyCanonicalHistory, guarded by turn epoch,
session id, and in-flight send submissions so it never clobbers a newer
turn or duplicates the blocking send path's own finalization.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.14

* fix(clients): filter non-chat models from chat pickers (#13317)

* fix(clients): filter non-chat models from chat pickers

* fix(clients): align chat model eligibility

* fix(desktop): strip user_input envelope when copying a user message (#13369)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>

* docs: show DeepSeek V4 peak and off-peak pricing (#13312)

* docs: update DeepSeek V4 average pricing

* docs: show DeepSeek peak and off-peak pricing

* docs: add GLM-5.3 reference pricing (same as GLM-5.2)

* docs: add GLM-5.3 to ClinePass models table

* fix(llms): display billed gateway cost (#13385)

* fix(shared): run PowerShell commands with fail-fast error semantics (#13358)

* fix(shared): run PowerShell commands with fail-fast error semantics

The run_commands PowerShell wrapper never set $ErrorActionPreference, so
the default 'Continue' applied: a pipeline erroring per item (e.g. a
malformed Where-Object over Get-ChildItem -Recurse) emitted one error
record per enumerated file - tens of thousands of stderr records on
large trees, looking like a hang - and could still resolve as SUCCESS
with exit 0.

Prepend $ErrorActionPreference='Stop'; to the script content executed
by the ScriptBlock so the first error terminates the command with a
non-zero exit and a single error message. Concatenated on the same line
as the user command so error line numbers stay unshifted.

Fixes #13285

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(shared): set the fail-fast preference in the bootstrap scope

Setting $ErrorActionPreference='Stop' by string-prepending it into the
scriptblock source displaced a leading param(...) from its mandatory
first-statement position, so scripts beginning with a param block failed
with CommandNotFoundException. Preference variables are dynamically
scoped, so setting Stop in the -Command bootstrap gives the invoked
scriptblock identical fail-fast semantics while keeping the user script
byte-identical (param works, error positions unshifted) and drops the
doubled-quote escaping.

* docs(shared): document the fail-fast tradeoffs in the PowerShell wrapper

Stop promotes every non-terminating error, not only per-item pipeline
floods: partial-result commands (recursive listings over access-denied
junctions) now stop at their first error, and Windows PowerShell 5.1
turns in-script stderr redirection of succeeding native commands fatal.
State this in the wrapper comment as a deliberate tradeoff, with the
GitHub Actions precedent and the per-command opt-outs.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>

* ci: stop over-long changelogs from silently dropping release Slack posts (#12955)

Slack section blocks reject text longer than 3000 characters. The Slack
action logs that rejection as ##[error] but does not fail the step, so an
over-long changelog drops the release announcement while the run stays
green — cline@3.0.50 (3272 chars) published to npm, tagged, and cut a
GitHub release with no Slack post and nothing red to notice.

Every publish workflow pasted the changelog section verbatim into one
section block, so all six were exposed; the SDK, desktop, and extension
sections were only 150-350 chars under the ceiling.

Add a slack_content output alongside content: unchanged when the section
fits, otherwise trimmed on a line boundary with a link to the full
release notes. Only the Slack payload uses it — GitHub release bodies and
the desktop updater manifest still get the whole section.

* ci: tidy workflow cache config and job permissions (#13403)

Publish workflows now always do clean npm installs (no dependency
cache in their test gates), the e2e workflow's cache keys are
exact-match only, and the e2e job drops an id-token permission it
never used.

* Rename desktop app from "Cline Code" to "Cline" (#13401)

* Rename desktop app from Cline Code to Cline

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Format touched Rust test assertions

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(llms): surface provider-executed tool activity as observational events (#13300)

* fix(llms): surface provider-executed tool activity as observational events

Provider-executed tool parts (e.g. every tool the Claude Code CLI runs
inside its own session) were dropped by the model-tool guard added for
web search: only declared model tools were re-emitted, everything else
hit continue with nothing yielded. Those sessions modified the workspace
with no tool activity in runtime events, transcripts, or the UI.

Route all providerExecuted parts onto the observational path instead:
emit execution-tagged tool-call-delta and tool-result events, matched by
tool-call ID for providers that omit the flag on the result half. They
stay out of AgentRuntime's execution/approval loop, and the runtime
already persists them as modelToolActivities and projects them for
display.

The AgentModelEvent tool-result variant widens toolName from
ModelToolName to string to carry the provider's own tool names.

* fix(agents): keep turns that are only provider-executed tool activity

A turn consisting solely of observational tool activity has an empty
assistant content array - the activity lives in message metadata, since
projecting it into content would replay tool_use blocks the model never
gets results for. The empty-content guard threw on such turns, erroring
the run and losing the activity from the transcript. Count model-tool
activity as content for the emptiness check (error finishes still
throw); replay stays safe through the codec's empty-content placeholder.
Also drop the trailing text delta from one gateway test so the tool-only
stream shape stays covered end to end.

* feat: allow agents to create scheduled tasks (#13331)

* feat(core, desktop): add durable todo agenda

* fix(desktop): secure todo approvals and track tool usage

* fix(desktop): clean up failed approval delivery

* fix(desktop): authenticate approval connections

* fix(desktop): cancel approvals on broadcast failure

* fix(desktop): authenticate development approvals

* fix(desktop): harden development approvals

* test(core): make task paths cross-platform

* fix(desktop): serialize approval readiness

* refactor(core): unify todo and schedule tools

* feat(core): distinguish user todos from agent suggestions

* fix(core): hide tasks tool in yolo mode

* fix(core): enforce schedule workspace scope

* fix(core): bind schedule scope to hub connection

* fix(core): establish task scope at hub startup

* fix(core): scope task automation by workspace

* test(core): normalize workspace path expectations

* test(core): serialize Windows CI workers

* fix(core): reject unregistered schedule authority

* fix(desktop): guard task execution commands

* fix(core): avoid polynomial regex in mention parsing

* fix(core): address schedule tool review feedback

* fix(core): bind websocket clients to hub workspace

* fix(core): flatten tasks tool input schema

* fix(core): authorize multi-workspace hub clients

* test(core): type hub transport authority mock

* fix(cli): register a workspace client for remote schedule commands (#13398)

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): treat ClinePass as OAuth-managed in the chat credential gate (#13404)

* fix(desktop): treat ClinePass as OAuth-managed in chat credential gate

ClinePass shares the Cline account OAuth credentials (its auth handler
stores under the "cline" provider), so the webview never sees a plain
API key for it. The chat pre-flight check only exempted cline/oca/
openai-codex, so switching to ClinePass while signed in via OAuth
blocked with "Missing API key" even though the sidecar resolves the
stored access token fine (which is why the CLI worked).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* style: format helpers.test.ts with biome

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): stack code block lines when streamdown lineNumbers is off (#13412)

streamdown renders each Shiki token line as a bare inline span with no
newline text between non-empty lines, and only applies its block line
class when lineNumbers is on. With lineNumbers off (the desktop app's
config) every multi-line fenced block collapsed into one run-on line.
Make the direct line spans under code-block-body display: block in the
shared markdown.css; empty lines keep their height via their lone "\n"
child under white-space: pre.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): work summary undercounts wall time when pre-tool thinking attaches to the answer (#13413)

* fix(desktop): anchor work summary duration on the answer row, not attached pre-tool reasoning

The collapsed 'Worked for Xs' row undercounted wall time whenever a turn's
assistant message contained thinking + tool_use with no narration text: the
canonical projection emitted the reasoning-only row after the tool row (both
stamped before the tool executed), the webview attached that row to the final
answer, and collapseCompletedWork used the answer's earliest attached
reasoning timestamp as the end anchor - excluding the entire tool execution
(e.g. 'Worked for 5s' for a turn with an 8s command).

- webview: end the work span at the answer row's own timestamp, clamped to
  the last collapsed row so a fallback answer bubble with a synthetic early
  timestamp cannot shrink the duration either
- sidecar: flush pending thinking before a tool_use row so rehydrated
  transcripts keep the live-stream order (thinking before its tool call) and
  pre-tool reasoning no longer rides on the next answer

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep interleaved thinking between the tool calls it separates

Address Greptile review: when one assistant message interleaves thinking
between multiple tool_use blocks, each reasoning segment now projects at its
own position (attached to a text row from its own segment when present,
otherwise as its own row) instead of merging into the first reasoning row,
which displayed later thinking before a tool call it actually followed.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): remove settings gear hover state while Account screen is open (#13408)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't show "No sessions found" while session history is still loading (#13414)

* fix(desktop): don't show 'No sessions found' while session history is still loading

Replace the isLoadingHistory flag with hasLoadedHistory, set only once the
backend has actually answered a list_discovered_sessions request. The sidebar
and Sessions view now keep their loading state until that first definitive
response, so the empty-state copy can no longer appear while history is still
being fetched (or while a failed fetch is being retried).

Also retry a failed initial fetch on the 2s event cadence instead of stranding
the UI until the 12s periodic poll, which is what stretched the misleading
empty state to ~10 seconds after a webview reload when the websocket lost the
race with the page load.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop history fast-retry from re-arming after hook unmount

A failed initial fetch that settles after the hook unmounted could schedule a
new retry timer after cleanup had already cleared the refs, leaving the
abandoned hook polling the backend every 2s. Guard scheduleRefresh with a
disposed ref set by the mount effect's cleanup.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix @ file mentions breaking on paths with spaces (#13391)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with "command not found" on VS Code 1.134 (#13402)

* Fix @ file mentions breaking on paths with spaces

Quote mentions generated by getFileMentionFromPath (Add to Cline /
Fix / Explain / Improve commands) when the relative path contains
spaces, so the mention regex no longer truncates the path at the
first space. Also quote the path part of workspace-prefixed mentions
(workspace:/path with spaces) inserted from the @ context menu, which
previously bypassed quoting because the value does not start with '/'.

Fixes #13338

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix import ordering in mentions test (biome organize imports)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Reduce fix to minimal scope

Revert the webview quoting refactor and extra tests; keep only the
getFileMentionFromPath quoting fix with a single regression test.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Normalize mention paths to posix separators for Windows

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix code actions failing with 'command not found' on VS Code 1.134

Code action commands carried arguments (expandedRange, diagnostics),
which routes them through VS Code's CommandsConverter cache. VS Code
1.134 disposes the cached entries before the clicked action executes,
so every lightbulb action failed with 'Actual command not found,
wanted to execute cline.addToChat'.

Drop the arguments so the command id is passed through directly, and
recover the context in the handler instead: getContextForCommand now
expands an empty selection by 3 surrounding lines (matching the old
provider behavior) and gathers document diagnostics intersecting the
range when none are passed explicitly.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Scope gathered diagnostics to the selection/cursor

Match the old CodeActionContext.diagnostics behavior: only include
diagnostics intersecting the range the action was requested for, not
the surrounding lines the text gets expanded to.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Desktop: unify Plugins, MCP, and Skills into one Plugins hub with a dedicated Marketplace page (#13411)

* Unify desktop plugins, apps, MCP, and skills into one Plugins hub with a Browse directory mode

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Open the marketplace directory as a modal over the Plugins hub instead of swapping the page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Rename directory to Marketplace: Browse Marketplace button, Marketplace modal title with icon, search placeholder

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Fix search input focus ring clipped by the Marketplace modal scroll container

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Address Greptile review: keep selected tag chip visible when its count drops to zero, and remount installed tab when a marketplace install completes after the modal closed

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Track marketplace modal mutation flag in a ref so a close click racing a queued render cannot skip the inventory remount

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Make Marketplace its own settings page under Customizations and restore Channels as a standalone page

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Remove icon from Marketplace page header for consistency with other settings pages

* Notify mounted inventory views when the marketplace invalidates the cache so late install completions refresh the Plugins hub

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop/ui): recommended and free model tiers in the composer model selector (#13410)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK (#13415)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): recommended-feed badges and descriptions in provider settings (#13416)

* feat(ui): sectioned model picker support in SearchCombobox

Adds option sections with headers, badges (NEW/Free pills), keyboard
navigation (arrows/Home/End/Enter with active-row tracking and
aria-activedescendant), substring match highlighting, a configurable
panel width, a trigger chevron, and a cleaner borderless search row.
All additions are backwards compatible; bumps @cline/ui to
0.2.0-next.6.

* feat(desktop): recommended and free model tiers in the composer picker

The composer's model selector showed raw provider/model ids and listed
the entire catalog alphabetized by id. It now labels providers and
models by display name and, for the cline provider, leads with the
Recommended and Free tiers from the recommended-models feed (NEW/Free
badges, descriptions) ahead of an All models section — matching the
CLI's featured picker and the kanban selector. cline-pass gets
Subscribed/Free tiers. A new list_cline_recommended_models sidecar
command exposes @cline/core's fetchClineRecommendedModels (display-ready
names, bundled offline fallback); feed ids resolve against the catalog
with a unique-slug fallback for Vercel/OpenRouter alias spellings, and
unresolvable entries are dropped rather than rendered unselectable.

* fix(desktop): widen the provider trigger for display names

Provider labels are now display names (e.g. "Cline Usage-Billing"),
which truncated badly at max-w-28.

* chore(desktop): drop unused featured-models test helper

* style(desktop): align workspace/branch picker search rows with the model picker

The composer's workspace/branch popover and the welcome screen's
workspace and branch pickers used a boxed inner search shell that now
clashed with the model picker's borderless search row sitting next to
them. Behavior unchanged.

* feat(ui): center the selected option when SearchCombobox opens

Opening a long list previously scrolled the selection just into view at
the panel edge; it now lands centered, and keyboard/hover navigation
falls back to minimal nearest-edge scrolling.

* style(desktop): picker row contrast, transparent search fields, centered open

The workspace/branch pickers' rows had a nearly invisible
surface-hover-lighter hover; rows now hover with surface-hover and mark
the current entry with the accent background plus check, matching the
model picker. The search inputs drop the Input base class's
dark:bg-input/30 tint that rendered a gray box inside the panel in dark
mode. Opening a picker now centers the current workspace/branch via a
shared scroll helper instead of starting at the top of the list.

* fix(ui): visible option hover/selected states and no scroll-jump on hover

The option row stacked bg-transparent with the conditional state
backgrounds; at equal specificity the later-sorted bg-transparent
utility won, so hover/selected rows rendered with no background at all.
The background classes are now mutually exclusive.

Mouse-driven active-row changes also reused the keyboard scroll-into-
view effect: hovering a row at the panel edge scrolled it into view,
which moved the list under the cursor and re-triggered hover — an
endless jump. Scroll mode is now per-source: center on open, nearest
for keyboard/typing, none for hover.

* fix(desktop): show only subscribed and free tiers in the cline-pass picker

The ClinePass offer is exactly the feed's subscribed + free tiers, but
stale bundled/cached catalog entries (e.g. a nemotron model) leaked
into an "All models" tier. Match the CLI's featured picker: hide
catalog leftovers, and only fall back to the full catalog when the
subscribed bucket is empty so a subscriber is never limited to free
models offline.

* fix(ui/desktop): strengthen the selected-row highlight in light mode

The selected row used the semantic accent surface (violet step 3),
which is nearly white in light mode. SearchCombobox and the desktop
workspace/branch pickers now highlight the selected/current row with
accent step 4 (with a fallback to --accent), which reads clearly in
both themes without touching the shared --accent token that shadcn
hover states depend on.

* fix(desktop): fit full provider display names in the composer trigger

"Cline Usage-Billing" — the default provider — truncated to
"Cline Usage-Bi…" at max-w-36; the trigger now allows up to max-w-56,
which fits the longest built-in provider names.

* style(ui/desktop): animate picker panels open like the shadcn dropdowns

The thinking-effort Select (shadcn/Radix) animates open while the
model/provider/workspace/branch pickers popped in instantly. All picker
panels now share the same open treatment — 150ms fade + slight zoom,
sliding from the trigger side. SearchCombobox uses a self-contained CSS
keyframe (consumers may not ship tw-animate-css); the desktop's custom
panels use the app's tw-animate utilities. Both respect
prefers-reduced-motion.

* chore(desktop): drop stale eslint-disable comments in picker search rows

This repo lints with biome; the jsx-a11y/no-autofocus disables were
inert leftovers. Flagged in review.

* refactor(core/desktop): stamp recommended-feed tiers onto ProviderModel in the SDK

Review feedback on the composer picker: tier joining should live where
the SDK serves model lists so each client doesn't fetch and join the
recommended-models feed itself (the CLI and now the desktop each did).

ProviderModel gains description and featured ({tier, rank, tags});
getLocalProviderModels overlays the feed's recommended/free tiers onto
cline models and subscribed/free onto cline-pass via
applyClineFeaturedModels, matching feed ids through the
Vercel/OpenRouter alias rules. The feed access is a new cached wrapper
(getCachedClineRecommendedModels, 5-minute TTL, in-flight dedupe) —
this path runs on every picker open, and the bundled offline fallback
is cached too so offline users don't re-pay the 5s timeout per list.

The desktop webview now reads tiers straight off the models: the
list_cline_recommended_models sidecar command, the webview feed fetch,
and its unique-slug alias matching are all deleted. toProviderModel
also carries ModelInfo.description generally.

* feat(desktop): recommended-feed badges and descriptions in provider settings

Review suggestion on #13410: the provider settings page has room for
more model detail than the composer's picker. The cline/cline-pass
provider cards now refresh their model list through
list_provider_models (the catalog snapshot deliberately skips the
recommended-feed overlay so the startup catalog fetch never blocks on
the feed) and render Recommended/Free tier badges plus feed tags (NEW)
next to the model name, with the model description underneath. The
refreshed list also surfaces the live entries instead of the bundled
snapshot.

* fix(ui): hand focus back to the combobox trigger on selection, close on Tab

Selecting an option (Enter or click) unmounted the focused search input
without a new focus target, dropping keyboard users' focus to <body> —
only Escape restored it. And since the search input is the panel's only
tabbable element, Tab always moved focus outside the component while
leaving the popup open behind the new focus target.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): keep the composer model selection inside the picker's visible offer

The active/remembered model was validated against the provider's full
catalog while the picker can intentionally hide models (the ClinePass
offer is exactly its subscribed/free tiers), so a stale remembered model
could become the selection while being absent from the dropdown.

Remembered and default selections (including on provider switch) now
resolve against the picker's visible options, and an explicitly
configured model that falls outside the offer stays active but is
surfaced under a 'Current model' section so the selection is always
visible and re-selectable.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): scope the settings featured model list to its provider and revision

The fetched featured list was unscoped component state: switching
between cline and cline-pass reused the component instance, so the
previous provider's models stayed visible while the new request was
pending (or forever, when it failed), and the retained copy shadowed
later provider.modelList updates — adding a second custom model
submitted the stale list as the complete configuration and dropped the
first addition.

The fetched list now only applies to the provider and modelList
revision it was fetched for (falling back to the catalog snapshot
otherwise and refetching on membership changes), and add-model submits
the union of the displayed and configured ids so an update can never
silently unconfigure existing entries.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): stamp featured tiers onto the provider catalog synchronously

listLocalProviders deliberately skipped the feed overlay so the catalog
never blocks on the network — but that left the composer's very first
picker open after a cold boot rendering an untiered flat list until the
per-provider fetch landed. Blocking was never required: stamp tiers from
a synchronous peek at data already in memory (the cached live feed when
fresh, else the bundled fallback, whose recommended ids resolve against
the bundled cline catalog). The per-provider model-list path still
refreshes with live feed data moments later.

* fix(core): harden featured-tier matching and the feed cache reset

Review findings on the tier overlay:

Vendor-prefix mismatches now match by unambiguous id slug (two-pass, so
a catalog carrying both spellings of a model stamps one row, and a slug
shared by two feed entries stamps nothing) — the bundled fallback feed's
vendor-prefixed ids can otherwise miss cline-free/-prefixed catalog
entries, leaving them untiered in degraded mode.

resetClineRecommendedModelsCacheForTests now bumps a generation so an
in-flight feed request resolving after a reset cannot repopulate the
cache it just cleared.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(ui): update packed-Tailwind smoke contract for the picker's max-h-64 (#13421)

The ui-publish smoke check pins a set of Tailwind candidates the packed
sources must emit; #13410 grew the SearchCombobox options list from
max-h-56 to max-h-64, so the publish run failed on the stale candidate.
All other pinned candidates verified against the current sources.

* chore(desktop): cut 0.0.15-beta.1

* chore(desktop): reconcile Cargo.lock with merged dependencies

* feat(desktop): refresh app icons and branding (#13400)

* chore(desktop): restore ai dependency and refresh lockfile

* chore(desktop): align beta cloud copy with the Cline rename

* chore(desktop): merge refreshed app icons (#13400) and dedupe changelog

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: JasmineLCY <38378321+JasmineLCY@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>
Co-authored-by: Renee Huang <100229782+reneehuang1@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
Co-authored-by: Haley Park <haleypark.design@gmail.com>
2026-08-20 10:31:39 -07:00
John Choi 3188816d91 feat(desktop): add cloud handoff and environment selection (#13359)
* feat(core): add cloud handoff primitives

* feat(desktop): add cloud handoff environment experience

* fix(desktop): preserve cloud image previews during sync

* fix(desktop): harden cloud handoff recovery

* fix(desktop): gate cloud handoff

* fix(desktop): use Cloud sessions gate for handoff
2026-08-19 15:33:10 -07:00
10c747663d chore(desktop): sync latest main into desktop experimental (#13379)
* fix(vscode): continue the surviving session on resume instead of rebuilding with the original task text (#13175)

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilt the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

The preserved conversation history is the source of truth on resume, so
the fallback prompt now just asks the model to reassess the history and
continue, matching the legacy resume prompt which also never resent the
original task. User-typed text still takes precedence when provided.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): continue the surviving idle session on follow-ups instead of rebuilding

Stopping a turn keeps the session alive, but every idle follow-up (bare
Resume after Stop, and typed follow-ups after a completed turn) tore
that session down and rebuilt it from persisted task history before
sending. Continue the matching idle session in place instead, the same
way the CLI reuses the live session after an abort. Rebuilding from
history now only happens when no live session matches the displayed
task (task opened from history, extension host reload).

A bare resume still needs a prompt to start a turn, so it sends the
neutral [TASK RESUMPTION] prompt (shared with the rebuild fallback and
hidden from the transcript); user-typed content is echoed and sent
as-is. If the send lands while the abort is still settling, the runtime
auto-queues it and drains once the abort completes.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): consolidate follow-up send paths in SdkFollowupCoordinator

Now that idle follow-ups continue the live session in place, the
two-mode sendToActiveSession helper was redundant: its non-queued branch
duplicated continueIdleSession minus the bare-resume prompt. Split it
into a single-purpose queueToActiveSession and fold the idle no-task
send into continueIdleSession, flattening askResponse's decision tree
to: queue onto a running turn, continue a matching live idle session,
rebuild from history, or abandon.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(vscode): reuse the existing neutral resumption prompt for bare resumes

Drop the newly invented long resumption wording in favor of the phrase
that already existed as the no-history fallback and that the transcript
hiding logic and test fixtures recognize: '[TASK RESUMPTION] Please
continue where you left off.' The net change to resumeSessionFromTask
against main is now just deleting the branch that resubmitted
historyItem.task as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): stop resubmitting the original task text on bare resume (#12975)

A bare Resume after Stop rebuilds the session from task history and
injected historyItem.task into the resumption prompt as 'New
instructions from the user'. The model treated the already-completed
original request as fresh instructions and re-executed it (e.g. re-ran
all terminal commands after stopping a queued follow-up turn).

Bare resumes now always use the neutral prompt that already existed as
the no-history fallback; user-typed text still takes precedence. This
matches the legacy resume prompt (responses.taskResumption), which only
ever included user-supplied text as new instructions.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): hide synthetic prompts from the queued-prompt echo

A send that races a settling abort is auto-queued by the runtime, so a
bare Resume can reach the pending_prompt_submitted echo carrying the
synthetic [TASK RESUMPTION] prompt. Echoing it leaked model-facing text
as a visible user bubble and shifted the visible-user-message ordinals
that edit/regenerate mapping relies on. Filter synthetic prompts with
isSyntheticUserPrompt, keeping user attachments visible (matching
isSyntheticSdkUserMessage semantics).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): preserve LiteLLM input token limits (#13293)

* fix(vscode): preserve LiteLLM input token limits

* fix(vscode): prefer live LiteLLM model metadata

* fix(vscode): generalize private catalog metadata

* test(vscode): preserve llms exports in vscode lm mock

* fix(vscode): point provider signup URLs at their API key pages (#13337)

* fix(vscode): point Mistral signup URL at the general API keys console

The Mistral provider's signup link led to the Codestral console, which
issues Codestral-scoped keys that fail with 401 on api.mistral.ai — the
endpoint the provider actually calls. Point it at the general API keys
page instead.

Fixes #13288

* fix(vscode): deep-link DeepSeek and Fireworks signup URLs to their API key pages

Both pointed at marketing homepages; link straight to the key-creation
pages instead, matching the rest of the registry and the desktop app's
provider-key-urls map.

* fix(ci): always build the legacy bundle from the legacy-extension branch (#13349)

The combined-VSIX workflow took legacy-ref as a free-form dispatch input
with no publish-time validation (next-ref has one: publish requires main).
Any typed ref — a PR merge ref, an unprotected branch — would be built
into the published VSIX by the environment-less build job, and the publish
environment approver only ever sees an opaque prebuilt artifact, so the
approval protected the marketplace PAT but not the shipped bytes.

Remove the input entirely and hardcode the protected legacy-extension
branch, which makes that branch's protection rules load-bearing for
releases. The tested-sha pinning between test-legacy and build is
unchanged. publish-extension skill dispatch command updated to match.

* fix(ci): lock the legacy publish workflow to the legacy-extension branch (#13350)

The branch dispatch input was a free-form string with no validation. Both
jobs checked it out and ran full npm lifecycle scripts from it: the publish
job next to VSCE_PAT/OVSX_PAT (and npm run publish:marketplace executes a
script from that same ref with the PATs in env), and the test job with NO
environment approval at all while inheriting the workflow-level
contents/packages/checks/pull-requests write grants. A dispatch pointing at
e.g. refs/pull/N/head would run outside-contributor code with the
marketplace keys behind one approval, or with a repo-write token behind
none.

Remove the input and hardcode the protected legacy-extension branch, drop
the workflow-level permissions to contents: read, and elevate only the
publish job to contents: write (tag push + GitHub release). The branch
input's default was legacy-extension, so normal publishes are unchanged.
publish-extension skill dispatch command updated to match.

* fix(vscode): SDK remote-config parity — refresh coordination, session gating, and fail-closed opt-out (#13226)

* feat(desktop): native notifications (#13166)

* feat(desktop): native notifications

* macos target

* fix(desktop): isolate macOS dev app identity

* fix(desktop): address notification review feedback

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched (#13310)

* fix(vscode): clear task-scoped settings overlay when task view is cleared or switched

Toggling an auto-approve setting while a task is open writes
autoApprovalSettings into the StateManager's task-settings overlay
(updateAutoApprovalSettings -> setTaskSettings). The SDK controller never
cleared that overlay on clearTask/showTaskWithId (the legacy controller
did), so after New Task the stale overlay kept shadowing global settings
in getGlobalSettingsKey(): toggle RPCs were accepted into global state,
but every posted state still carried the overlay's old version, which the
webview rejects as not newer - the auto-approve checkboxes froze forever.

Restore legacy parity in SdkTaskControlCoordinator: drop the overlay
(persisting pending writes first) in clearTask() and before installing a
different task's proxy in showTaskWithId().

Fixes #13260

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* changeset

* test(vscode): add end-to-end regression test for auto-approve freeze after New Task

Wires the real StateManager, the real updateAutoApprovalSettings handler,
and the real SdkTaskControlCoordinator.clearTask() together with the
webview's version gate modeled on ExtensionStateContext, pinning the
end-to-end invariant behind #13260: checkbox toggles must keep reaching
the webview after a mid-task toggle followed by New Task. Verified the
test fails when the clearTaskSettings() call is removed from clearTask().

* fix implicit any in regression test

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): show provider web-search support under the settings toggle (#13328)

* feat(desktop): show provider web-search support under the settings toggle

The global Web search toggle silently does nothing unless the session's
provider offers native web search, which made the setting read as if it
worked with any provider. The desktop General settings row now explains
that only providers with built-in web search honor it, and shows a live
status line: which connected providers are ready to use it (no extra
setup needed), or an amber warning with a link to the Models section
when none of them support it.

Support is resolved in the webview via a new providerOffersModelTool
helper in @cline/llms (browser export), sharing the same builtin-manifest
source of truth as the runtime's supportsModelTool attachment check.

* fix(desktop): address review — refetch web-search status on catalog invalidation, clarify per-model support

Greptile P2: the one-time catalog fetch could race an in-flight provider
save and show stale status; the row now refetches when the provider
catalog cache is invalidated (fired after saves complete).

Greptile P1: the ready line implied every model on the provider works;
Vertex excludes Claude routes, so the copy now scopes the promise to
models that support it.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* feat(ui/desktop): collapse finished runs into a work summary and remove hover-state dead space (#13315)

* feat(ui): add WorkActivity collapsed-run summary and float message actions as a pill

WorkActivity/WorkActivityTrigger/WorkActivityContent fold a finished agent
run's working rows (tool calls, thinking traces, narration) behind a single
"Worked for 4m 12s · 14 tool calls" disclosure built on the shared animated
disclosure primitives, with formatWorkActivityLabel/formatWorkDuration
exported for consumers.

Message hover actions no longer rely on the transcript reserving blank space
below each message: the action row is now a self-backed pill (border,
blurred background, shadow) that floats over whatever follows, so
conversations can pack rows tightly without hover chrome colliding with the
next message.

* feat(desktop): collapse finished runs into a work summary and tighten chat spacing

collapseCompletedWork post-processes the grouped transcript: once a run ends
on assistant text with no further tool calls, its working rows fold into one
expandable WorkActivity row while the final answer stays visible. Runs are
delimited by user messages; the trailing run only collapses when the session
has stopped running and actually produced an answer, so live streams and
cancelled/failed tails keep their rows. Assistant messages carrying images
or media are treated as deliverables and never collapse.

The conversation list gap drops from gap-8 to gap-4 now that hover actions
are self-backed pills that need no reserved space, and user messages add
their own top margin so turn boundaries stay visually distinct.

* refactor(ui/desktop): work summary label wording, flat expansion, stable in-run rhythm

Feedback round on #13315:

- Label reads "Worked for 4m 12s and made 14 tool calls" instead of joining
  with a dot; without a duration it falls back to "Made N tool calls".
- Expanded work rows render at transcript level — no rail or extra indent —
  since tool rows and thinking traces already carry their own nesting when
  expanded. The work content keeps the tight working-row rhythm.
- Live working rows (thinking traces + tool calls) now group into a 'run'
  render item with the same tight 0.25rem rhythm, so there is no oversized
  gap under a "Thought for Ns" row and every row keeps its exact position
  when the finished run folds into the work summary. A trailing
  answer-in-progress stays outside the group at transcript level, and pure
  prose spans keep normal spacing.
- The transient "Thinking..." indicator moves inside the transcript column
  and mirrors a trigger row's geometry, so the first real row replaces it in
  place with no jump.

* style(ui/desktop): hover-pill metrics, right-pointing work chevron, scroll and spacing fixes

Another feedback round on #13315:

- Hover action pill: +2px internal padding, a trailing inset after the
  timestamp (it sat flush against the pill border), and more clearance
  between the message content and the pill (2px -> 6px; the hover bridge
  grows to match).
- The work summary chevron points right while collapsed and continues
  counterclockwise to point up when expanded.
- Conversation bottom padding drops pb-20 -> pb-8: the composer sits below
  the scroller, so the padding only needs to clear a pinned action pill.
- Sending a message scrolls back to the bottom even if the reader had
  scrolled up (new AutoScrollOnSend on the user-message count, which ignores
  optimistic-bubble re-keying; @cline/ui now exports useConversation for
  this).
- An assistant answer directly under its run's working rows pulls itself
  0.5rem closer than the full transcript gap.

* style(desktop): leave a visible gap between a pinned action pill and the composer

pb-8 exactly matched the pill's ~40px footprint, so the last row's hover
actions sat flush against the composer top; pb-12 restores ~8px of daylight.

* style(desktop): widen the gap between the pinned action pill and the composer to ~24px

pb-12 left only ~8px of daylight under the pill; pb-16 reads comfortable
without reverting to pb-20's dead space.

* fix(desktop): keep the thinking indicator at the working-row offset mid-run

The indicator matched a trigger row's geometry but sat a full transcript gap
(1rem) below the last working row, while the tool/thinking row replacing it
joins the tight run group at 0.25rem — a visible upward jump. When the last
transcript item is working rows (or streamed assistant output), the
indicator now pulls up to the same tight offset; only at the start of a run,
under the user message, does it keep the normal gap.

* style(ui): calm the hover actions surface per team feedback

Borderless rectangle instead of the bordered pill: radius drops to
var(--radius), the side padding goes entirely (the icon buttons carry their
own hit areas), and the vertical padding halves. Blurred background and
shadow stay so it remains legible over following content.

* feat(ui/desktop): full-band hover reveal and iOS-style disclosure easing

The hover actions only appeared while the pointer was inside the message
box itself. The invisible bridge under each message now spans the full
height of the band the floating actions occupy (full row width), so
hovering anywhere in that strip reveals them. Sibling row types
(.cline-chat-tool, .cline-chat-work, and the desktop's run/tool groups)
become position: relative so they paint above the bridge — their own
content keeps its hover and clicks, and the bridge only wins in the band's
genuinely empty space.

All expandable rows (work summary, tool panels, thinking) open and close on
a 240ms symmetric ease-in-out cubic-bezier instead of the 60ms snap, with
chevron rotation on the same curve. Reduced-motion still disables both.

* revert(ui/desktop): drop the full-band hover reveal; quicken disclosure easing to 180ms

The full-band hover bridge (and the position: relative changes that made it
safe) is reverted per feedback — back to the narrow bridge that only spans
the gap under the message. The iOS-style ease-in-out on disclosures stays
but speeds up from 240ms to 180ms.

* fix(ui): recover live tool diffs that mount as a blank pierre skeleton

Live-streamed edit rows could show an empty diff for the whole run, with the
diff only appearing after the collapsed work row was expanded (fresh mount).
Root cause, confirmed by driving a live session and inspecting the element:
React StrictMode double-invokes @pierre/diffs' ref callback; the first
instance's async highlight work aborts on its immediate cleanup, and the
second instance adopts the abandoned half-rendered shadow tree as if it were
complete prerendered output — zero height, no code, no theme stylesheet,
permanently. A rendered diff always carries style[data-theme-css] in its
shadow root, so ToolFileDiff now checks for it shortly after mount and
remounts FileDiff (bounded attempts) when missing; the fresh host element
takes the normal render path and recovers within ~400ms. Verified live: the
diff now renders during the run.

* fix(desktop): keep interrupted runs expanded even with partial trailing text

The trailing-run collapse gated on 'ended with assistant text', which
misread a Stop that landed mid-answer as a finished run and folded the tool
calls the user wants to inspect. The gate is now the terminal status itself:
only completed (or restored-idle) sessions collapse the trailing run;
cancelled/failed/error tails keep their rows regardless of partial text.
(Greptile P1 on #13315 — matches the PR's stated rule.)

* feat(ui): share the markdown pipeline, chat polish, and ThinkingBlock across products (#13323)

* feat(ui): share the markdown pipeline, chat polish CSS, and ThinkingBlock

The desktop app and the cloud dashboard both consume @cline/ui yet rendered
assistant output differently, because Markdown policy and the thinking-trace
row lived app-side. This moves the shareable parts into the package:

- components/markdown (new export): the lazy Shiki code highlighter (GitHub
  light/dark, pinned language set) and agentMarkdownControls — the standard
  Streamdown configuration. streamdown/shiki/@shikijs/* become optional peer
  dependencies, mirroring @pierre/diffs.
- components/markdown.css: the desktop's chat polish moves in — chat-scale
  headings, outside list markers, single quiet code blocks with a
  hover-revealed copy control, table cards. Kept unlayered so it beats
  Streamdown's layered Tailwind utilities without !important.
- ThinkingBlock + formatThoughtLabel in agent-chat: the standard thinking
  row (brain icon, Thinking/Thought-for-Ns label, streaming shimmer, rail
  presentation, capped scrollable body). The shimmer and the
  reasoning-hover-suppression rule move into agent-chat.css; triggers gain
  the color transition the desktop applied locally.

Version bumps to 0.2.0-next.5 for the dashboard to pick up.

* refactor(desktop): consume shared markdown and thinking primitives from @cline/ui

The local Shiki highlighter, Streamdown controls, chat markdown polish CSS,
streaming-title shimmer, and reasoning hover-suppression rule are deleted in
favor of the @cline/ui versions (the highlighter test moves to the package's
suite). ReasoningBlock becomes a thin wrapper that hands MemoizedMarkdown to
the shared ThinkingBlock, and formatThoughtLabel re-exports from the package
so grouping code and tests keep their import path.

globals.css now imports @cline/ui/components/markdown.css (unlayered, so the
polish keeps beating Streamdown's layered utilities); the app keeps only what
is genuinely app-specific: link/image policy in markdown.tsx, selectability
rules, accent palettes, and the view-enter transition.

* style(ui/desktop): make thinking-trace prose legible

Thinking body text rendered too faint: plain muted-foreground plus the
desktop's font-thin weight. The shared thinking content now leans 75% of the
way back toward the body text color (still slightly de-emphasized), and the
desktop drops the thin font weight.

* ci(ui-publish): build @cline/shared before ui typecheck (#13354)

@cline/ui's generated-media imports @cline/shared/browser, which resolves to
shared's dist output. The build-shared step sat after typecheck/test/build,
so the first ui-publish dispatch since #13025 failed at Typecheck UI with
TS2307. Move the step to right after install.

* fix: run_commands object form without args routes through the shell instead of failing with ENOENT (#13336)

* fix: run_commands object form without args routes through the shell

The structured { command, args? } form of run_commands was always spawned
directly with shell: false. When a model emitted a full command line in
command with no args (e.g. { command: "echo hello" }), spawn failed with
ENOENT for any command containing a space, breaking command execution for
the whole session.

Direct exec now only applies when a non-empty args list is provided; the
object form without args is routed through getShellInvocation like the
string form. Schema descriptions are tightened so models put arguments in
args instead of embedding them in command.

Fixes #13279

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: trim structured-command schema descriptions

The union schema is only used for lenient validation of input the model
already sent; its descriptions never reach a model prompt. Keep them
short instead of restating executor behavior.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore: simplify direct-exec comment in shell executor

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* revert: keep original structured-command schema description

The description never reaches a model prompt and the executor now handles
both shapes, so the wording change was cosmetic noise.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: gate direct exec on args key presence, not array length

Review feedback: an explicit empty args array is intentionally structured
input and stays direct exec; only an object with no args key is treated
as a full shell command line. Matches the key-presence rule already used
by the VS Code host's formatCommandForTerminal. Also replaces the
empty-args shell test (which was PowerShell-incompatible) with a test
pinning the direct-exec contract.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: normalize Gemini custom base URLs for legacy host-root values (#13329)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* docs: add GLM-5.3 to ClinePass models and reference pricing (#13357)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(desktop): stream run command output (#13179)

* feat(desktop): stream run command output

* fix(sdk): clean up detached command logs

* fix(sdk): reap detached logs after hub restarts

* fix(sdk): preserve live detached command logs

* fix(desktop): harden live command progress

* fix(sdk): recover detached logs for local hosts

* fix(desktop): reconcile command output tool rows

* fix(sdk): retain logs for surviving commands

* fix(core): prevent PID reuse from retaining detached logs

* fix(core): preserve detached logs on probe failures

* fix(core): retain detached logs during probe outages

* fix(desktop): resolve leftover merge conflict in messages projection test

Combine both sides of the assertion: main's incremented per-block
createdAt projection and this branch's toolCallId/hookEventName meta.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): make TUI dialog colors follow theme changes live (#13355)

* fix(cli): make TUI dialog colors follow theme changes live

Dialog content previously read the static palette constant, so open
dialogs (including the theme picker itself) kept the default dark-blue
accents while scrolling through theme previews. Add getDialogPalette /
useDialogPalette, which resolve dialog colors from the active theme's
dialog accents and re-render on every theme change, and migrate all
dialog-rendered components to it.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(cli): derive dialog panel background from the active theme

Dark themes now lift their own background one OKLAB step for the dialog
surface, so panels keep the theme's hue instead of the library's fixed
#262626. DialogThemeSync pushes the surface into the dialog container
for new dialogs and repaints open panels, so the surface also follows
live theme previews. Light themes keep the neutral dark panel to match
the dark accent fallback and the light-on-dark dialog text.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix: skill slash commands load via the skills tool instead of expanding into the user message (#13327)

* fix(desktop): show typed slash command instead of expanded skill markdown

The sidecar expands /skill and /workflow tokens into their instructions
before dispatching, so the runtime's persisted transcript only contains the
expanded text. After a turn (and when reopening a session) the webview
re-hydrates from that history and rendered the whole SKILL.md body as the
user's message; queue events echoing the expanded prompt could also add a
second user bubble, and fresh sessions were titled with the markdown's first
line. The CLI never shows this because its TUI keeps the typed text in its
own transcript and only sends the expanded prompt to the model.

Mirror that separation inside the desktop sidecar's display boundaries:

- history projection (readSessionMessages) inverts user text that starts
  with a configured command's instructions back to '/name remainder',
  which also repairs sessions recorded before this fix
- queue snapshots and chat_queued_prompt_start events echo the typed
  prompt recorded at expansion time, so the webview's optimistic-bubble
  re-key matches again
- an untitled session sent an expanded prompt gets titled from the typed
  command instead of the instructions' first line

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): don't overwrite a mid-turn rename with the typed-command title

The untitled check ran before dispatch, so renaming a fresh slash-command
session while its first turn was running got clobbered by the post-turn
typed-command title. Re-check at write time and only replace a missing title
or the one the runtime auto-derived from the expanded prompt.

Also documents the inherent prefix-inversion ambiguity flagged in review:
text hand-typed with a command's exact instructions persists byte-identically
to that command's expansion, so stored history alone cannot distinguish them.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): stop expanding skill commands; let the skills tool load them

Pasting the skill body into the prompt is why the transcript could ever show
it: the desktop webview re-hydrates from the runtime's persisted history, so
whatever the sidecar splices into the user message renders as if the user
typed it. The runtime already registers the skills tool, whose description
requires the model to invoke it whenever the user references a slash command
— so send the typed /skill text through and let the tool deliver the
instructions as a tool result (previously they arrived twice: pasted and via
the tool). The persisted user message, session title, and queue entries are
then simply the typed command, which deletes the typed-prompt registry, the
queue event/snapshot rewriting, and the title machinery from the previous
approach.

Workflows are not served by the skills tool and keep textual expansion, so
the read-time display inverter stays: it collapses expanded workflow prompts
— and skill prompts persisted before this change — back to the typed
/command in the history projection.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* feat(core): option to keep skill slash commands typed for the skills tool

resolveRuntimeSlashCommandFromWatcher (and the hub snapshot proxy) accept
expandSkillCommands: hosts whose sessions register the skills tool pass
false so the typed /skill goes through and the model loads the instructions
as a tool result, keeping the persisted transcript as what the user typed.
Workflows always expand — the tool does not serve them. isSkillsToolAvailable
exposes the catalog check hosts use to decide (yolo preset and the skills
tool toggle leave textual expansion as the only delivery path).

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): skill slash commands load via the skills tool instead of expanding

The TUI user-command wrap and buildUserInputMessage now keep a typed /skill
as-is when the session's mode/toggles register the skills tool, matching the
desktop app; workflows keep expanding, and yolo (zen) keeps expanding skills
because its preset has no skills tool. This also fixes CLI resume/history
surfaces showing the skill body: the persisted user message is now the typed
command.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(vscode): keep configured skill slash commands typed for the skills tool

expandSlashCommands no longer splices a configured skill's instructions into
the model text; the SDK session's skills tool delivers them as a tool result
(previously they arrived twice). Builtin pseudo-skills like /deep-planning
are not served by that tool and keep expanding, as do workflows.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): use the shared skill-expansion option in the sidecar

Replaces the sidecar's workflow-detection dance with core's
expandSkillCommands option and gates on isSkillsToolAvailable, restoring
textual expansion where the tool is missing (yolo mode or the skills tool
toggle) — a gap in the previous desktop-only change.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* refactor(desktop): drop the display inverter for expanded transcripts

Accepted trade-off to keep the change minimal: sessions recorded before
skills switched to the skills tool, workflow sends (deprecated), and
yolo-mode skill sends persist expanded instructions and now render that text
as-is instead of being collapsed back to the typed /command at projection
time.

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>

* Use fixed selection chevron in account dialog to match other dialogs (#13364)

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): align system prompt with session mode (#13361)

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>

* fix(desktop): finalize queued turns on chat_done with canonical history reconcile (#13330)

Turns that settle through the event stream (queued prompts, including the
first prompt of a fresh session) resolve their send() RPC early, so nothing
cleared the streaming shimmer or reconciled live-streamed content against
the persisted transcript at turn end. A turn whose deltas were incomplete
stayed visually streaming forever and only healed when a later non-queued
send rehydrated history.

chat_done (and chat_session_ended / the queue-drain double check) now clears
the active assistant streaming id and schedules a short-delayed
read_session_messages + applyCanonicalHistory, guarded by turn epoch,
session id, and in-flight send submissions so it never clobbers a newer
turn or duplicates the blocking send path's own finalization.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* chore(desktop): release v0.0.14

* fix(clients): filter non-chat models from chat pickers (#13317)

* fix(clients): filter non-chat models from chat pickers

* fix(clients): align chat model eligibility

* fix(desktop): strip user_input envelope when copying a user message (#13369)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>

* test(vscode): stabilize code action activation

* test(vscode): activate code action by keyboard

* test(vscode): decouple action discovery from invocation

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Co-authored-by: JasmineLCY <38378321+JasmineLCY@users.noreply.github.com>
Co-authored-by: Mikołaj Kondratek <19799111+mkondratek@users.noreply.github.com>
Co-authored-by: Max <maxpaulus43@gmail.com>
Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bee <abeatrix@users.noreply.github.com>
2026-08-19 13:58:37 -07:00
Saoud Rizwan b43fb7777c Merge PR #13069 head (cloud models, prompt dedupe, org scope fixes) into desktop-experimental
# Conflicts:
#	apps/examples/desktop-app/sidecar/chat-session.ts
2026-08-17 18:27:06 -07:00
John Choi 8ea71e3278 fix(desktop): align cloud header status 2026-08-17 18:23:38 -07:00
John Choi f4abb7e14b feat(desktop): load Cline Cloud agent models 2026-08-17 18:16:56 -07:00
John Choi 1e08d8f277 fix(desktop): refresh cloud organization scope 2026-08-17 18:16:49 -07:00
John Choi 8bebcd1f37 Merge remote-tracking branch 'origin/main' into work/pr-13069-four-fixes
# Conflicts:
#	apps/examples/desktop-app/webview/components/agent-sidebar.tsx
2026-08-17 18:07:11 -07:00
John Choi 14f9def7ed fix(desktop): reconcile terminal cloud replies 2026-08-17 17:56:42 -07:00
John Choi 2e6c25785e fix(desktop): dedupe live cloud prompts 2026-08-17 17:41:06 -07:00
Saoud Rizwan b565e5b9ca Merge PR #13069 head (cloud provisioning/handoff fixes) into desktop-experimental
# Conflicts:
#	apps/examples/desktop-app/webview/hooks/use-chat-session.ts
2026-08-17 17:36:14 -07:00
Saoud Rizwan 0313c34b1a chore(desktop): release v0.0.14-beta.1 2026-08-17 17:29:42 -07:00
Saoud Rizwan d0f49330a3 Merge remote-tracking branch 'origin/main' into desktop-experimental
# Conflicts:
#	apps/examples/desktop-app/webview/components/agent-sidebar.tsx
#	apps/examples/desktop-app/webview/components/views/settings/settings-view.tsx
2026-08-17 17:27:25 -07:00
John Choi db31380268 Merge remote-tracking branch 'origin/main' into work/pr-13069-four-fixes 2026-08-17 17:18:21 -07:00
Saoud Rizwan b123f2eb54 Merge remote-tracking branch 'origin/main' into desktop-experimental 2026-08-17 16:21:56 -07:00
John Choi 303722a559 fix(desktop): show prompt during cloud provisioning 2026-08-17 14:34:43 -07:00
John Choi 5fff9cbb4c fix(desktop): preserve prompt during cloud handoff 2026-08-17 12:30:16 -07:00
John Choi 70e0a3731d fix(desktop): harden cloud provisioning recovery 2026-08-17 12:18:18 -07:00
John Choi d52af3112d Merge remote-tracking branch 'origin/pr/13069' into feat/experimental-desktop-pr-bundle
# Conflicts:
#	apps/examples/desktop-app/webview/app/page.tsx
2026-08-14 19:21:03 -07:00
John Choi eccb0db669 fix(desktop): recover cloud provisioning handoff 2026-08-14 19:17:50 -07:00
John Choi a9af97a83f fix(desktop): preserve cloud session routing with SSH environments 2026-08-14 19:16:41 -07:00
John Choi 53388f5f9c Merge remote-tracking branch 'origin/pr/13069' into feat/experimental-desktop-pr-bundle
# Conflicts:
#	apps/examples/desktop-app/sidecar/cloud-sessions.test.ts
#	apps/examples/desktop-app/sidecar/commands.ts
#	apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx
#	apps/examples/desktop-app/webview/components/views/settings/settings-view.tsx
#	apps/examples/desktop-app/webview/hooks/use-chat-session.test.tsx
#	apps/examples/desktop-app/webview/hooks/use-chat-session.ts
2026-08-14 19:00:05 -07:00
John Choi 093e9dd3d5 fix(desktop): harden cloud session creation 2026-08-14 18:54:31 -07:00
John Choi d35f4a043b fix: reconcile experimental desktop feature stack 2026-08-14 18:34:07 -07:00
John Choi bd9ee23f6c Merge origin/main into desktop cloud sessions 2026-08-14 18:32:55 -07:00
John Choi 10ff456e93 Merge remote-tracking branch 'origin/pr/13225' into feat/experimental-desktop-pr-bundle 2026-08-14 17:58:48 -07:00
John Choi cb34ba5cf9 Merge remote-tracking branch 'origin/pr/13031' into feat/experimental-desktop-pr-bundle 2026-08-14 17:58:44 -07:00
John Choi 2ed4b811be Merge remote-tracking branch 'origin/pr/12890' into feat/experimental-desktop-pr-bundle 2026-08-14 17:53:04 -07:00
John Choi 7bf542ddab Merge remote-tracking branch 'origin/pr/13069' into feat/experimental-desktop-pr-bundle
# Conflicts:
#	apps/examples/desktop-app/sidecar/commands.ts
#	apps/examples/desktop-app/sidecar/session-data/messages.test.ts
#	apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.test.tsx
#	apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx
#	apps/examples/desktop-app/webview/components/views/settings/settings-view.tsx
#	apps/examples/desktop-app/webview/hooks/use-chat-session.test.tsx
#	apps/examples/desktop-app/webview/hooks/use-chat-session.ts
2026-08-14 17:43:00 -07:00
abeatrix 1cb61ab925 Merge remote-tracking branch 'origin/main' into bee/agent-voice
# Conflicts:
#	apps/examples/desktop-app/package.json
#	apps/examples/desktop-app/sidecar/commands.ts
#	apps/examples/desktop-app/webview/app/globals.css
#	apps/examples/desktop-app/webview/app/page.tsx
#	apps/examples/desktop-app/webview/components/agent-header.tsx
#	apps/examples/desktop-app/webview/components/agent-sidebar.tsx
#	apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx
#	apps/examples/desktop-app/webview/components/views/chat/chat-messages.test.tsx
#	apps/examples/desktop-app/webview/components/views/chat/chat-messages.tsx
#	apps/examples/desktop-app/webview/components/views/chat/welcome-chat.test.tsx
#	apps/examples/desktop-app/webview/components/views/chat/welcome-chat.tsx
#	apps/examples/desktop-app/webview/components/views/chat/workspace-selector.tsx
#	apps/examples/desktop-app/webview/components/views/settings/provider-list-view.tsx
#	apps/examples/desktop-app/webview/components/views/settings/settings-view.tsx
#	apps/examples/desktop-app/webview/hooks/use-chat-session.test.tsx
#	apps/examples/desktop-app/webview/hooks/use-chat-session.ts
#	apps/examples/desktop-app/webview/lib/desktop-tray.test.ts
#	apps/examples/desktop-app/webview/lib/desktop-tray.ts
#	apps/examples/desktop-app/webview/lib/session-history.ts
#	bun.lock
#	sdk/packages/llms/package.json
#	sdk/packages/llms/src/catalog/catalog.generated.ts
#	sdk/packages/llms/src/providers/providers.generated.ts
#	sdk/packages/shared/src/llms/ai-sdk-format.ts
#	sdk/packages/ui/package.json
2026-08-13 15:54:16 -07:00
John Choi 7bf495878e fix(desktop): harden cloud session lifecycle 2026-08-13 15:52:04 -07:00
Tomás Barreiro 677a5cd915 Merge branch 'main' into add-integrations-onboarding-step 2026-08-13 19:05:59 -03:00
Tomás Barreiro 4524e884a7 Merge branch 'main' into add-integrations-onboarding-step 2026-08-13 15:42:32 -03:00
BarreiroT 75d1d6654b validate domain and fix errors on auth 2026-08-13 14:56:45 -03:00
BarreiroT 45027ee4e3 Add a GitHub integration step to the onboarding 2026-08-13 14:33:14 -03:00
John Choi 7f8fd0b62d fix(desktop): harden cloud session synchronization 2026-08-13 08:16:53 -07:00
John Choi 6df53ddf64 Merge origin/main into desktop cloud sessions 2026-08-13 08:15:59 -07:00
John Choi 243b6fe9d9 Merge remote-tracking branch 'origin/main' into work/pr-13069-four-fixes
# Conflicts:
#	apps/examples/desktop-app/webview/app/page.tsx
#	apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.test.tsx
#	apps/examples/desktop-app/webview/components/views/chat/chat-input-bar.tsx
#	apps/examples/desktop-app/webview/components/views/chat/chat-messages.tsx
#	apps/examples/desktop-app/webview/components/views/chat/welcome-chat.tsx
#	apps/examples/desktop-app/webview/components/views/chat/welcome-workspace-controls.test.tsx
#	apps/examples/desktop-app/webview/components/views/chat/welcome-workspace-controls.tsx
#	apps/examples/desktop-app/webview/hooks/use-chat-session.test.tsx
#	apps/examples/desktop-app/webview/lib/workspace-paths.ts
2026-08-12 09:11:57 -07:00
John Choi edd6e2c29b fix(desktop): close cloud session interaction gaps
Forward image attachments, allow session-scoped model changes, recover queued streaming after abort, and surface relayed run failures.
2026-08-10 16:49:03 -07:00
Saoud Rizwan 1eb28e553e test(desktop): make the pagination-recovery test deterministic
Waiting on rendered text could pass in a stale window between the search
clearing and the base reload effect running, letting the scroll fire a
stale observer whose captured request key mismatched. Drop the captured
observer callback before clearing and wait for the effect to recreate it,
which only happens after the post-clear base list applied.
2026-08-08 18:58:56 +00:00
Saoud Rizwan 42225f9219 refactor(desktop): consolidate cloud-session helpers before review
No behavior changes; review-readiness cleanup of the cloud sessions diff:

- one cloudRepositoryLabel helper in webview/lib/cloud-repositories.ts
  replaces four copies of the owner/repo label parser (sidecar placeholder
  title, repository picker, composer context label, provisioning phases)
- the cloud-provisioning- placeholder id prefix moves behind
  isCloudProvisioningSessionId, shared by the sidecar that mints the ids
  and the webview affordance gates that check them
- the two identical cloud status mappers in use-chat-session collapse into
  mapCloudRuntimeStatus in chat-session/helpers.ts
- attach() and attachExpired() share one attachResultPayload builder
  instead of duplicating the reply literal
- settings-view drops the commented-out PostHog lookup block in favor of a
  short pointer comment
- welcome-chat derives its fallback connect URL from the environment
  config instead of hardcoding production
- refresh the stale claim-set comment in create-recovery to describe the
  post-fix wait-all semantics
2026-08-08 18:58:55 +00:00
Saoud Rizwan da759853ea test(desktop): wait out the post-clear branch reload before paginating again 2026-08-08 05:37:23 +00:00
Saoud Rizwan 6fed0f0d38 style(desktop): format new test fixtures 2026-08-08 05:34:04 +00:00
Saoud Rizwan b456ce73dd fix(desktop): webview polish for cloud sessions from deep review
- humanize cloud error envelopes in the sync-failed banner and the
  rehydration fetch fallback (and scope the fallback to the active session)
- align the recovered-send status mapper with the rehydrated handler:
  cover cancelled, and leave unknown statuses alone instead of flipping a
  running turn to completed
- disable Delete on provisioning placeholders in the sidebar and sessions
  view; the sidecar always rejects it until the create settles
- surface the CLINE_CODE_CLOUD_AGENTS override in Settings when it makes
  the toggle diverge from effective behavior, and load the settings
  sections concurrently
- gate the slash-command menu to local sessions like @-mentions; the
  sandbox cannot resolve local skills/workflows
- stop the model selector from silently 'correcting' a locked cloud
  session's model when its id is missing from the local catalog
2026-08-08 05:32:53 +00:00
Saoud Rizwan 2b9d1c3e6a fix(desktop): harden cloud session lifecycle edges
- reap connections whose session vanished from a successful list (deleted
  remotely or re-scoped): they otherwise redial the dead proxy every ~5s
  forever, with a REST list per attempt, until app restart
- clear desktop-visible state synchronously at the start of dispose() so
  a manager rebuilt mid-dispose (account/credential change) cannot have
  its fresh liveSessions/pendingApprovals entries deleted from under it
- report rehydrated failed runs with the same chat_session_ended reason
  (error) the live run.failed path uses
2026-08-08 05:32:53 +00:00
Saoud Rizwan 82277128aa fix(desktop): only honor Connect GitHub URLs from Cline app origins
The cloud error envelope travels in Error.message and is authenticated by
string prefix only, so error strings a session pod controls (hub command
replies pass through verbatim) could spoof a github_not_connected envelope
whose connectUrl pointed anywhere. The webview rendered that as a trusted
looking Connect GitHub button and open_external_url validates protocol,
not origin. Drop connectUrls whose origin is not a known Cline app base
URL before they reach the action button.
2026-08-08 05:26:59 +00:00
Saoud Rizwan ee4edfeaf6 fix(desktop): keep the cloud setup snapshot fresh across org switches
The stale-selection guard compared repoUrl against a repositoryUrls
snapshot refreshed only on mount, account-id change, focus, or the
onboarding poll (which stops in ready status). An in-app org switch
refreshed none of those, so picking a repository from the new scope's
correctly filtered picker got immediately wiped against the old scope's
list. Route the picker's own loads through the same request-id-guarded
snapshot application, and re-check setup on the sidecar's
cloud_sessions_changed broadcast.
2026-08-08 05:25:00 +00:00
Saoud Rizwan 17b0ad7a76 fix(desktop): close both directions of the create-recovery claim race
Recovery previously waited only for earlier identical peers, so an
earlier create failing fast (any request_failed, including an instant
5xx) could adopt a later in-flight POST's listed session and hand two
composers the same sandbox. Branchless and branch-specific creates also
hashed to different claim keys while the branchless recovery filter
ignores branch, allowing cross-key adoption with no ordering at all.

- key in-flight peers by repo/model/org (branch excluded) so
  branchless recoveries see branch-specific peers
- settle each create's peer entry when its POST settles (never after
  recovery), then make recovery wait for every other in-flight peer in
  both directions, re-snapshotting until stable; waits cannot cycle
- gate recovery on timeout/5xx/no-status failures: a fast 4xx never
  provisioned anything, and recovering on one risks adopting an
  identical-config session created by another device on the account
2026-08-08 05:21:55 +00:00
Saoud Rizwan be8bb302a8 fix(desktop): re-key the first cloud prompt's bubble to the server session id
A cloud create returns a server-assigned session id, but the optimistic
user bubble kept the client-planned id. mergeCloudSnapshotWithLive drops
other-session messages before consulting the optimistic map, so the first
prompt's bubble silently lost its retention semantics: a lagging snapshot
could merge to a transcript with no user prompt, and a failed first send
lost its bubble on the next rehydration.

Also pins the previously untested merge behaviors: the reflected-prompt
budget (zero-budget retention and one-consumption-per-new-copy) and
error-bubble preservation on the unmatched-live drop path.
2026-08-08 05:18:45 +00:00
Saoud Rizwan f4f573c395 fix(desktop): release the branch picker's loading flag when a page fetch goes stale
loadMore reset loadingMore only when the request key still matched. Typing
a search character while a page fetch was in flight changed the key, so
the stale fetch never released the flag and pagination was dead for the
rest of the welcome screen's life (the observer effect and loadMore both
short-circuit on loadingMore). Only one page fetch can be in flight, so
the reset can be unconditional.
2026-08-08 05:16:29 +00:00
Saoud Rizwan e03b4577b4 fix(sdk): close the hub client zombie-connection race around registration
The post-registration continuation was the one mutation window not guarded
by the connect generation: a close() landing after the register reply
resolved but before the continuation ran would mark a closed client
registered. That stale flag then made a later failed registration skip
closing its socket, leaving a permanently unregistered zombie connection
that isConnected() reported healthy.

- generation-guard the continuation so a superseded attempt closes its
  socket and rejects instead of touching shared state
- drop the registered-flag condition from the connect() catch guard; the
  socket identity check alone decides ownership and cannot be poisoned
- keep a stale attempt's late timeout/error/close handlers from clobbering
  lastCloseError and sawSocketClose for a newer attempt
- stop close() from wiping the real connect failure cause when no socket
  was ever opened
2026-08-08 05:15:03 +00:00
Saoud Rizwan 16d36e53f2 Merge branch 'main' into saoudrizwan/desktop-cloud-sessions-9a9b 2026-08-07 21:50:08 -07:00
Saoud Rizwan 74a1910505 Merge branch 'main' into saoudrizwan/desktop-cloud-sessions-9a9b 2026-08-07 20:50:44 -07:00
Saoud Rizwan f3db7a0a98 fix(desktop): order concurrent identical creates so recovery cannot steal an in-flight session
A session is listed the moment the server starts provisioning it, minutes
before its successful POST returns. Timeout recovery now waits for every
earlier identical in-flight create to record its claim before adopting a
listed candidate; later peers wait on earlier ones only, so waits cannot
cycle. Regression test covers the slow-success/fast-failure overlap.
2026-08-08 03:21:07 +00:00
Saoud Rizwan 2b62f7b447 fix(desktop): webview review fixes for cloud sessions
- Emit cloud_session_provisioning_failed from the sidecar and render a
  terminal error pane in an open placeholder thread instead of an
  infinite provisioning spinner.
- Cloud-aware delete confirmations in the sidebar and sessions view (the
  action destroys the remote workspace, not just local history).
- Humanize cloud rename failures instead of showing the raw envelope.
- Preserve UI error bubbles through cloud rehydration merges; ignore
  unknown snapshot statuses instead of flipping a running turn to done.
- Clear a stale repository selection when the account can no longer
  access it so the send gate re-engages.
- Migrate cloud optimistic bookkeeping across queued-prompt re-keys,
  clear cloud refs on reset, session-scope the cloud merge, fix the
  impure provisioning-phase updater, gate rename on provisioning
  placeholder rows, and stop advertising local-only mentions/commands in
  cloud composer placeholders.
2026-08-08 03:03:15 +00:00
Saoud Rizwan 6b8b5726c3 fix(desktop): cloud session lifecycle hardening from deep review
- Reap connections whose sandbox expired (attach, sidebar poll, and
  reconnect-failure paths) so dead sessions stop reconnect-looping and
  spamming sync-failure events; sync failures now notify on transition
  only.
- Tombstone sessions mid-delete so a concurrent attach/send cannot dial a
  fresh connection that outlives the delete; treat remotely-gone sessions
  (404/410) as deletable locally.
- Guard disposed connections against resurrection by late reconnect timers
  and approval responses; purge approvals stored during failed connection
  setup.
- Leave cloud approvals pending on app shutdown instead of denying tool
  calls on pods that outlive the app.
- Use a fresh auth token (with fallback) for create-timeout recovery;
  normalize list rows so one malformed record cannot crash discovery;
  widen the recovery clock-skew window now that claims prevent
  double-adoption.
- Drop the queue-shrink 'prompt started' inference on the hub path (the
  hub emits explicit submitted events; a shrink can also mean removal).
- Reset the transcript baseline on reconnect; answer pendingPrompts with
  [] for sessions with no inner session instead of throwing.
2026-08-08 02:55:37 +00:00
Saoud Rizwan f6d8e54089 fix(desktop): cross-cutting review fixes for cloud sessions
- Use core's canonical getProviderAuthHandler("cline") for the persisted
  token fallback instead of a hand-rolled prefix heuristic that could
  corrupt unprefixed API keys; drop the dead test-only reset export.
- Reset the cloud session manager and broadcast cloud_sessions_changed
  after a cline OAuth login, and broadcast on the save_provider_settings
  (sign-out) reset, so the sidebar re-scopes immediately.
- Log cloud discovery failures instead of silently emptying the sidebar.
- Atomic write-then-rename for the desktop settings file.
- Share the repository/branch wire types between sidecar and webview.
- Add command-layer tests for the settings/flag commands; refresh the
  stale sidecar ARCHITECTURE.md; delete an orphaned comment.
2026-08-08 02:47:00 +00:00
Saoud Rizwan ef09caa3e6 fix(sdk,desktop): harden hub header resolution and onboarding poll
Review findings: bound resolveConnectionHeaders with the connect timeout so
a hung token refresh cannot pin connect() and every deduped caller forever;
record resolver failures in lastCloseError so getConnectionError() reports
the real cause; add a connect-generation token so close() during header
resolution cannot leave a doomed attempt satisfying the next connect();
stop header-auth clients from inheriting registry tokens for loopback URLs;
use the shared extractSessionId in approval.list_pending. Desktop: make the
onboarding poll read status from a ref instead of running side effects in a
state updater, and re-check GitHub connectivity when the account changes.
2026-08-08 02:42:20 +00:00
Saoud Rizwan bdb5f30f5e refactor(desktop,sdk): remove the PostHog flag plumbing cloud sessions no longer use
Reverts the SDK feature-flags service/provider changes and barrel exports to
main, and strips the desktop sidecar's PostHog-backed flag service (context
targeting, cache file, refresh/dispose lifecycle). isCloudAgentsEnabled() is
now just the env override plus the Settings toggle, and get_feature_flags
answers synchronously.
2026-08-08 01:50:57 +00:00
Saoud Rizwan 2fa0be8940 refactor(desktop,sdk): drop the code-cloud-agents flag from the SDK catalog
Cloud sessions are gated by the explicit Settings toggle now, so the SDK no
longer registers the unused PostHog flag. The Settings row is wrapped in a
visibility gate that is hard-wired on, with the future flag lookup left
commented out until the flag actually exists in PostHog.
2026-08-08 01:21:40 +00:00
Saoud Rizwan 2e5a565090 fix(desktop): harden queue snapshot validation and recovery claiming
- applyQueueSnapshot now rejects replies without a prompts array instead of
  publishing an authoritative empty queue from the pending/update/remove
  command paths.
- Timeout-recovery candidate selection and claiming now happen in one
  synchronous helper so the claim can never be separated from the check,
  and the regression test exercises truly concurrent create requests.
2026-08-08 00:24:42 +00:00
Saoud Rizwan eb95908737 fix(desktop): only treat valid pending-prompts replies as authoritative queue snapshots
An unsuccessful or malformed session.pending_prompts reply during
rehydration no longer publishes an empty queue or discards buffered queue
events; the newest buffered queue snapshot is replayed instead.
2026-08-08 00:05:44 +00:00
Saoud Rizwan b091c72e40 fix(desktop): address cloud-session review findings
- Keep the newest buffered queue snapshot when rehydration's queue fetch
  fails instead of silently dropping queued/steered prompts (Greptile P1).
- Claim recovered/created session ids per process so overlapping identical
  create requests cannot adopt the same record and orphan a sandbox
  (Greptile P1).
- Use crypto.randomUUID() for provisioning placeholder ids (CodeQL
  insecure-randomness alerts).
2026-08-07 23:44:05 +00:00
Saoud Rizwan ebf5b52e9b Merge origin/main into saoudrizwan/desktop-cloud-sessions-9a9b
Resolves conflicts with #13028 (native-feel polish and render-path
performance): keep dynamic view imports and the memoized headerDiff from
main while preserving the cloud-session behaviors from this branch (Cloud
icon import, Connect GitHub error-action button in the chat error banner,
and hiding the diff header for cloud sessions).
2026-08-07 23:36:29 +00:00
Saoud Rizwan 13d801b578 fix(desktop): allow renaming cloud sessions from the sessions view
Rename was already supported by the sidecar and offered in the sidebar and
chat header; the sessions view context menu was the odd one out. Includes
biome format fixes picked up in touched files.
2026-08-07 22:52:44 +00:00
Saoud Rizwan 642c2f643b feat(desktop): cloud sessions onboarding panel for GitHub connect flow
When the cloud composer cannot start a session yet (signed out, GitHub not
connected, or the GitHub App has no repository access) replace the composer
with an onboarding panel that explains cloud sessions, walks through the
dashboard hand-off with visual steps, and auto-detects completion via polling
and window-focus refetches. Adds a teaching hint under the ready composer.
2026-08-07 22:52:44 +00:00
Saoud Rizwan 528c6b8ff2 feat(desktop): gate cloud sessions behind an explicit settings toggle
Cloud sessions are in preview, so replace the remote rollout flag with an
opt-in toggle in Settings -> General, persisted in a desktop-owned settings
file (kept out of global-settings.json so older CLI writers cannot strip it).
The CLINE_CODE_CLOUD_AGENTS env override still wins for development. Toggling
broadcasts feature_flags_changed so open composers react without a restart.
2026-08-07 22:52:36 +00:00
Saoud Rizwan 2f1c94505a Merge origin/main into desktop cloud sessions branch 2026-08-07 22:32:38 +00:00
John Choi 5b7005b003 fix(desktop): paginate cloud branch picker 2026-08-07 08:51:45 -07:00
abeatrix e48a7c9c91 feat(code): add SSH remote environments PoC 2026-08-07 00:20:13 -07:00
John Choi 71e1c59ab8 feat(desktop): switch models in cloud sessions 2026-08-06 20:38:53 -07:00
abeatrix d7b61de54f Merge remote-tracking branch 'origin/main' into bee/agent-voice
# Conflicts:
#	apps/examples/desktop-app/sidecar/context.test.ts
#	sdk/packages/llms/src/catalog/catalog.generated.ts
#	sdk/packages/llms/src/providers/ai-sdk.ts
#	sdk/packages/llms/src/providers/builtins.ts
#	sdk/packages/llms/src/providers/provider-ids.generated.ts
#	sdk/packages/llms/src/providers/providers.generated.ts
#	sdk/packages/llms/src/providers/routing/provider-option-rules.ts
#	sdk/packages/llms/src/providers/routing/provider-options.test.ts
2026-08-06 18:15:57 -07:00
John Choi 7350294f7d fix(desktop): harden cloud session lifecycle and flags 2026-08-06 10:01:27 -07:00
John Choi fe90cbfdf1 Merge remote-tracking branch 'origin/main' into codex/desktop-cloud-agents 2026-08-06 09:49:19 -07:00
John Choi 44cf50cee7 fix(desktop): cloud sessions follow the active account scope
The sidebar now shows exactly the active scope's cloud sessions (personal
or the active organization — matching the dashboard), instead of merging
both. On account/organization switch the sidecar broadcasts
cloud_sessions_changed so the sidebar re-scopes immediately rather than
on the next poll; the cloud manager reset already discards the org cache
and connections.

The session registry is now upsert-only: a session opened under another
scope stays routable (send/abort keep working) when the server-side
active org drifts mid-run, even though it leaves the visible list.
Display truth stays lastListedSessions (active scope only).

Known behavior: after a full account/org SWITCH (manager reset), stale
threads from the previous scope report session-not-found on cold reopen —
the list no longer shows them, so this is reachable only via stale
webview state.
2026-08-06 09:43:04 -07:00
John Choi b39c7e0319 feat(desktop,sdk): cloud transcript synchronization + approval recovery
Implements the agreed convergence design (mirrors experiment/mobile-app):
subscribe → buffer → attach → snapshot (messages/status/queue) → install →
replay unreflected events → live. Single-flight with one queued rerun;
failed snapshots never become an authoritative empty transcript;
segment-scoped substring supersession in the sidecar; multiset count-delta
optimistic reconciliation with first-hydrate gating in the webview.

Review-round fixes on top of the sync implementation:
- Recovery baseline advances on delivered sends — a lost duplicate prompt
  can no longer be falsely confirmed by an earlier identical delivery.
- Prompt occurrence matching normalizes the pod's <user_input> wrapper
  (real transcripts never matched raw prompts; tests used unwrapped
  fixtures, so recovery was inert in production).
- Streamed-text trim symmetry so whitespace cannot defeat supersession
  and duplicate an entire already-persisted reply on reconnect.
- Buffered queue snapshots are dropped during replay (always older than
  the synced queue; replaying could regress it and double-bubble).
- approval.list_pending advertised in HUB_CAPABILITIES (capability-gated
  clients could never discover it) and the sidecar's approvals refresh
  never wipes observed state unless the reply provably carries the list.
- Safety tests: replay-when-not-contained, whitespace supersession,
  queue-snapshot drop, wrapped-prompt recovery, baseline advance.

Tracked follow-ups (not in this change): approval.respond and event
delivery remain unscoped hub-wide (scoping naively would break the
desktop's second approvals client); sessionId is mandatory here vs
optional on the mobile branch.
2026-08-06 09:34:16 -07:00
abeatrix 378fdf147b feat: avatar overlay 2026-08-05 22:34:31 -07:00
John Choi 9c62406da9 fix(desktop,sdk): pre-PR review round — connection lifecycle and UX hardening
From a three-lens adversarial review of the branch:

Sidecar (cloud-sessions)
- BLOCKER: remove the connections-map entry when inner-session creation
  fails — the poisoned entry returned a disposed client whose event
  subscription was gone, silently streaming nothing for every later send.
- Single-flight inner-session creation: concurrent sends could fork two
  inner sessions on the pod, permanently dropping one run's events.
- Cache the active-organization lookup (60s, successes only) — the
  sidebar poll was making two authed REST calls per tick.
- delete() now drains an in-flight connect (zombie-connection race).
- Cold-cache expiry surfaces the clean session_expired envelope on
  send/read paths, not a raw WS upgrade failure.
- A provisioned-but-connect-failed create no longer reports failure for
  a live, billed sandbox; connect happens on demand instead.
- Guard against empty 2xx create responses (raw TypeError before).

SDK (hub client)
- Socket-identity guards in every connect-attempt cleanup path: a stale
  attempt's late timeout/error/close can no longer clobber a newer
  in-flight attempt's socket or dedupe state.

Webview
- Placeholder→real swap keeps the placeholder thread when opening the
  real session fails (was: deleted it and dumped the user on a blank
  fallback thread).
- Reset executionTarget to local when the cloud flag flips off on a
  fresh thread (was: permanently stranded cloud-gated composer).
- inferStatusFromMessages preserves 'provisioning' (the hydration pass
  was clobbering the sidebar state to idle within a second).
- Humanize cloud error envelopes in the delete toast and rename failure
  (rename previously had no catch at all).

Regression tests: poisoned-connection recovery, inner-session
single-flight.
2026-08-05 22:05:22 -07:00
John Choi a988d6a6ee fix(desktop): keep cloud loading continuous through the provisioning swap
The placeholder → real-session swap mounts a fresh thread whose hydration
briefly showed the skeleton between the provisioning row and the
conversation. An empty cloud session mid-hydration now shows the same
compact row ("Opening session...") so the loading treatment never
changes shape.
2026-08-05 21:36:52 -07:00
John Choi f1ec8c897c fix(desktop): cloud session dogfood round — billing, UX, and reliability
From live dogfooding of cloud agent sessions:

Billing & sessions
- Bill the user's ACTIVE organization (server-side active flag, cached
  resolver; personal fallback) instead of always personal credits, and
  list both personal and org-scoped sessions.
- Auto-title sessions from the first prompt; support rename via REST.
- Optional branch passthrough (picker + create body + recovery match).
- Forward autoApproveTools into cloud session creation.

Provisioning experience
- Sidebar placeholder while the synchronous create provisions (REST list
  cannot see the session yet), pulsing status dot, instant list nudge.
- Unified compact loading row (shared cycling phase line) for both the
  originating thread and the placeholder pane; phases advance once and
  hold rather than looping.
- cloud_session_provisioned event swaps placeholder threads to the real
  session when the sandbox is ready.
- Opening a placeholder is benign (loading state), reads return empty,
  only mutating actions error.

Correctness
- Surface run.failed error payloads in the chat (silent-failure fix; the
  raw CLOUD_SESSION_ERROR envelope can no longer reach the screen).
- Emit chat_session_status only on real status changes (pods stream
  periodic snapshots — every visited session was marked unread forever).
- expiredAt is a TTL deadline, not an end time; display uses createdAt
  (backend bumps updatedAt on every WS connect).
- provisioning is a first-class SessionHistoryStatus (the normalizer was
  collapsing it to idle).
- The new-prompt hero requires a thread WITHOUT a history session —
  fixes every flash-of-intro-screen path for existing sessions.
- Archived-history fallback only replaces a live failure when a snapshot
  actually exists (404 = null, not empty).

Plus GitHub repository/branch pickers, org-scoped integration URLs,
thinking-effort passthrough, and feature-flag targeting by account id.
2026-08-05 21:32:51 -07:00
John Choi 0b1f083531 fix(desktop): evaluate cloud-agents flag inside ChatThreadPane
The flag sync referenced ChatThreadPane state from Home, which only
surfaced at next-build prerender (tsconfig.dev does not cover webview).
2026-08-05 18:01:13 -07:00
John Choi d79ad49de3 Merge remote-tracking branch 'origin/main' into codex/desktop-cloud-agents 2026-08-05 17:58:02 -07:00
John Choi ce78c667cf feat(desktop): cloud agent sessions behind code-cloud-agents flag
Cloud sessions via core-platform remote-session API + Hub protocol v1:
sidecar CloudSessionManager (REST lifecycle, per-session NodeHubClient with
Bearer header auth, event translation, approvals, pending-prompt bridge,
expired-session archived history), webview Local/Cloud selector with repo +
optional branch picker, and a default-off PostHog feature flag gating
creation and the selector (existing sessions always remain accessible).
SDK: NodeHubClient resolveConnectionHeaders option; close unregistered
socket on registration failure; export FeatureFlag const from @cline/shared.
2026-08-05 17:57:57 -07:00
abeatrix 2140996303 feat(desktop): avatar pet
- Add native Tauri avatar-window lifecycle, positioning, tray integration, and persisted preferences.
- Add the `/avatar-overlay` webview route and v2 spritesheet animation runtime.
- Play the standard wave animation when the avatar first appears.
- Add bundled Cline Bot and Mom spritesheets.
- Add avatar discovery, selection, visibility, and manifest-precedence support.
- Add Settings controls for enabling and selecting desktop avatars.
- Add an animated voice orb driven by microphone intensity.
- Improve realtime voice panel visibility, mute/stop controls, playback coordination, and tests.
- Reorganize chat input controls so speech, realtime voice, stop, and send actions do not compete.
- Document avatar package structure and storage.
2026-08-05 14:19:28 -07:00
abeatrix 0d1a72f009 ui update 2026-08-05 12:13:53 -07:00
abeatrix a6c0cfd5c3 move audio-player to shared ui package 2026-08-05 11:19:05 -07:00
abeatrix 17e8c0cbc9 feat(desktop): add media player 2026-08-05 11:14:57 -07:00
abeatrix e2b97792b1 feat(desktop): play generated audio artifacts 2026-08-04 22:19:09 -07:00
abeatrix e4a7c19432 feat(desktop): add generated video support 2026-08-04 22:15:50 -07:00
abeatrix 589f32d13e feat(desktop): improve realtime voice sessions 2026-08-04 22:15:21 -07:00
abeatrix 879c085fdb feat(desktop): add realtime voice and UI refinements 2026-08-04 22:15:21 -07:00
599 changed files with 54039 additions and 44741 deletions
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Warn when attached images will be ignored because the selected model does not support image input: image thumbnails get a warning badge and the composer offers to switch to an image-capable model, instead of the images being silently dropped before the API call
+136 -131
View File
@@ -1,33 +1,34 @@
---
name: publish-extension
description: Use when releasing the Cline VS Code extension — stable (standalone SDK build of main via ext-vscode-publish-stable), nightly (ext-vscode-publish-nightly, manual dispatch), or an emergency legacy-branch hotfix (ext-vscode-publish-legacy). Guides version selection, changelog, workflow dispatch, environment approvals, tagging, post-publish verification, and the remaining retirement of the finished A/B rollout machinery.
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 an emergency legacy hotfix — or to retire the leftover A/B rollout machinery.
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: standalone SDK extension from `main`
## The current era: combined A/B rollout
**The legacy → SDK migration is complete.** The PostHog flag `ext-sdk-bundle-rollout` reached 100% (verified empirically 2026-09-15: 200/200 `/decide` probes returned `true`), so every user on the combined VSIX runs the `next` (SDK, bun) bundle and the `legacy/` half is dead weight. Stable releases now ship a **plain build of `main`** through `ext-vscode-publish-stable.yml`. The combined A/B path (`ext-vscode-ab-package.yml`) is no longer used for releases and is pending deletion — see "Retiring the A/B machinery" at the bottom for what is still left to clean up and the one caveat (leave the flag at 100%).
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).
History, for context only: the A/B era ran `4.1.0``4.1.17` (JulSep 2026). Design docs remain at `apps/vscode-rollout/README.md` and PR #12253 until that directory is removed.
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** | `saoudrizwan.claude-dev` | `ext-vscode-publish-stable.yml` | dispatch, from `main` | `apps/vscode/package.json` on `main`; tag `v<version>` must match |
| Nightly | `saoudrizwan.cline-nightly` | `ext-vscode-publish-nightly.yml` | **manual dispatch only** (cron deliberately removed) | auto `<major>.<minor>.<unix-ts>` from main's `apps/vscode/package.json` |
| Legacy hotfix (emergency only) | `saoudrizwan.claude-dev` | `ext-vscode-publish-legacy.yml` | dispatch | `apps/vscode/package.json` on `legacy-extension` |
| 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` |
Stable runs the reusable bun suite (`ext-vscode-test.yml`, tests `main`) before an environment-gated publish job (`publish` environment required reviewers approve in the Actions UI). Nightly uses `PublishNightly` (branch policy only). Nightly **still builds the combined loader VSIX** (loader + `next/` + `legacy/`) until it is converted back to a plain build — that conversion is on the retirement list below.
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` has been published from multiple workflows and 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:
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" \
@@ -36,123 +37,13 @@ Stable runs the reusable bun suite (`ext-vscode-test.yml`, tests `main`) before
| python3 -c "import json,sys; v=json.load(sys.stdin)['results'][0]['extensions'][0]['versions'][0]; print(v['version'], v['lastUpdated'])"
```
**`ext-vscode-publish-stable` has no marketplace-monotonicity preflight** (that check only ever lived in the retired ab-package workflow), so this query is the only guard. The `legacy-extension` branch sits at `4.0.12`, so it cannot collide with the `4.1.x` line, but a legacy hotfix would have to be numbered above the live stable version (see the emergency section).
`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. **Tag == package version, enforced.** The workflow reads `apps/vscode/package.json`, requires the `tag` input to equal `v<that version>`, and hard-fails otherwise. Bump the version on `main` first.
3. **Changelog lives at the repo ROOT** (`CHANGELOG.md`) — not `apps/vscode/CHANGELOG.md` (doesn't exist). The workflow hard-fails unless the first `## [` heading is exactly `## [<version>]`. The section body becomes the GitHub release notes and the Slack post (Slack copy is trimmed to 3000 chars with a link out; the release body stays whole).
4. **Ask before pushing** commits or tags. **Never approve the `publish` environment gate yourself via `gh api`** — hand the maintainer the run URL to click "Review deployments".
5. **Concurrency**: the workflow groups on the tag with `cancel-in-progress: false`. A publish run left `waiting` on approval blocks every later dispatch of the same tag until cancelled (`gh run cancel <id>`).
## Stable release — the current path
### Pre-flight
```bash
# 1. What's live (rule 1) → pick <VERSION> strictly above it (normally patch bump).
# 2. Confirm main's package.json is at the *previous* published version, i.e. the repo
# reflects the live line and nothing unreleased is already bumped:
node -p "require('./apps/vscode/package.json').version"
# 3. What's in the release:
git fetch origin main --tags
git log v<PREV>..origin/main --oneline --no-merges -- apps/vscode sdk/packages
```
The CLI/SDK notes are the best starting point for the extension notes — the extension bundles `@cline/*` from source, so an SDK release in the same window ships here too. Read `sdk/CHANGELOG.md` for the matching SDK version and translate what's extension-visible; skip CLI-only and desktop-only items.
### Release prep on `main` (PR, not direct push)
- Bump `apps/vscode/package.json` → `<VERSION>`.
- Prepend `## [<VERSION>]` to root `CHANGELOG.md` with the approved notes.
- Side effect of the bump: nightly versions become `<major>.<minor>.<unix-ts>` of the new base — harmless (separate listing, still monotonic).
Optional local rehearsal of the exact packaging step (the workflow has no dry-run input, and the build runs inside the gated publish job, so this is the only pre-approval check):
```bash
bun install --frozen-lockfile && bun run build:sdk
cd apps/vscode && npx @vscode/vsce package --no-dependencies --allow-package-secrets sendgrid --out /tmp/rehearsal.vsix
# vsce runs vscode:prepublish → `bun run package` (check-types + build:webview + lint + esbuild --production),
# i.e. the same build the workflow performs. Telemetry env is NOT set locally, so expect
# telemetry to be dark in this artifact — that is fine for a build rehearsal, not for shipping.
```
### Dispatch
```bash
gh workflow run ext-vscode-publish-stable.yml --ref main \
-f release-type=release \
-f auto_create_tag_from_main=true \
-f tag=v<VERSION>
gh run list --workflow=ext-vscode-publish-stable.yml --limit 1 --json databaseId,url,status
```
What the run does, in order: test gate on `main` → publish job checks out `main`, **creates and pushes `v<VERSION>` at the tested SHA** (auto-create mode; refuses if the tag already exists elsewhere) → `bun install --frozen-lockfile` → `bun run build:sdk` → asserts the `better-sqlite3` native binary → verifies tag/version/changelog/PATs → `vsce package` → `bun run publish:marketplace` (vsce publish **and** `npx ovsx publish`, both `--no-dependencies`) → GitHub release with the `.vsix` attached → Slack post. The publish job waits for `publish` environment approval before any of that. Check what a run is waiting on:
```bash
gh api repos/cline/cline/actions/runs/<run-id>/pending_deployments
```
`release-type=pre-release` publishes to the pre-release channel of the same listing; it is a real publish, not a rehearsal.
### Post-publish
1. Verify both registries serve the new version (expect minutes-to-an-hour of Marketplace validation lag after "Published" appears in the logs):
```bash
# Marketplace: query from rule 1
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. Verify the bookkeeping landed: `git fetch --tags && git tag --list 'v<VERSION>'`, `gh release view v<VERSION>`, Slack post in the release channel. Tag creation happens **before** the build in this workflow, so a tag-push failure fails the run early (no publish) rather than leaving a published-but-untagged release. Known cause: the built commit touches `.github/workflows/**` (default token cannot create such refs). Workaround: create and push the tag yourself at `origin/main` HEAD (ask first), then re-dispatch with `auto_create_tag_from_main=false` from `main` — the workflow requires the existing tag to point at the exact SHA it tested.
3. Artifact check (`gh run download <run-id>` or the release asset): `package.json` inside is `saoudrizwan.claude-dev@<VERSION>`; `grep -c 'process.env.TELEMETRY_SERVICE_API_KEY' extension/dist/extension.js` must be **0** (a leftover literal means the build ran without its env and telemetry is silently dead). `CLINE_ROLLOUT_VARIANT` is defined to `""` for ordinary builds by `apps/vscode/esbuild.mjs`, so no literal is expected and telemetry carries no `extension_variant` — correct for a standalone build.
4. Monitor errors on `extension_version = '<VERSION>'` in `otel.otel_logs` (stable cohort is cleanly separable — nightly versions are timestamps). Metabase dashboards 17 (task error rate) and 19 (error deep dive).
## Nightly release
**Manual dispatch only.** The cron was removed on purpose: the `PublishNightly` environment made scheduled runs sit `waiting`, hold the concurrency group, and silently cancel every later scheduled run behind them. A stale nightly listing is therefore expected, not a bug.
```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
```
No changelog/version prep — the version is computed. Verify with the Marketplace query against `saoudrizwan.cline-nightly`. Until converted, nightly still ships the combined loader VSIX with a `legacy/` bundle built from `legacy-extension`.
**Red run ≠ failed publish** on this path: its tag-push step runs *after* publishing and fails whenever main's HEAD touches `.github/workflows/**`. If "Published" appears in the logs, the release went out; push the `nightly-main-<UTC ts>-<sha12>` tag manually with user credentials.
## Emergency rollback
Preferred: **ship a fixed build of `main` at a higher version** through the stable workflow above. It is the same path, fully gated, and the only rollback that keeps users on the SDK extension.
Last resort — the legacy hotfix path — still exists but is degraded: `legacy-extension` is the pre-SDK npm codebase, last touched 2026-08-18 at `4.0.12`, and a publish from it would move every user back onto code that is weeks behind. If it is ever needed:
```bash
# On legacy-extension: commit the fix, bump apps/vscode/package.json ABOVE the
# live stable version (rule 1 — e.g. 4.1.18 live -> hotfix is 4.1.19, 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
# (the branch is hardcoded to legacy-extension in the workflow)
```
The npm suite runs ungated, the publish job waits on the `publish` environment, the workflow tags and creates the GitHub release itself, and it publishes to Marketplace **and** Open VSX. On that branch use `npm`, never `bun`, and expect the old monolith layout (`apps/vscode/src/core/...`).
## Retiring the A/B machinery (still to do)
The rollout is done but the scaffolding is still in the repo. Retire it in this order, each as its own PR:
1. **Nightly → plain build of `main`**: drop the loader/`legacy-src` stitching from `ext-vscode-publish-nightly.yml` so nightly matches stable. Preserve the `|| 'default'` fallbacks for `inputs.*` while editing.
2. Delete `ext-vscode-ab-package.yml` and, once the emergency path above is judged unnecessary, `ext-vscode-publish-legacy.yml`; keep the `legacy-extension` branch for history.
3. Remove `apps/vscode-rollout/` and the rollout-only code paths in `apps/vscode/src/services/telemetry/rollout-metadata.ts` (the `extension_variant` metadata and `extension.rollout.bundle_activated` event).
4. Optionally port the marketplace-monotonicity preflight from the old ab-package workflow into `ext-vscode-publish-stable.yml` — the only automated guard for rule 1 was retired with it.
5. **Archive the PostHog flag last, and not yet.** Machines still on a combined VSIX (`≤ 4.1.17`) consult `ext-sdk-bundle-rollout` on every window load and treat a *deleted* flag as `legacy`. Leave it at 100% until `extension.rollout.bundle_activated` for combined versions flatlines, then archive. Re-verify the percentage empirically before touching it (no PostHog admin needed — the key is inlined in any shipped combined loader; download the 4.1.17 VSIX from the Marketplace `vspackage` URL, `gunzip`, `unzip`, `grep -o 'phc_[A-Za-z0-9]*' extension/extension.js`):
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];
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) {
@@ -167,15 +58,129 @@ The rollout is done but the scaffolding is still in the repo. Retire it in this
})()' "$KEY"
```
6. Update this skill: delete this section and the combined-loader notes under Nightly.
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 publish=true
# (the legacy bundle always builds from the protected legacy-extension branch;
# it is deliberately not an input)
# 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
# (the branch is hardcoded to legacy-extension in the workflow; it is
# deliberately not an input)
```
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
- `bun run package` in `apps/vscode` does not build `@cline/*` workspace deps — fresh checkouts need `bun run build:sdk` first (the workflows handle this).
- Every ext workflow pins `bun-version: 1.3.14` while the root `packageManager` is `bun@1.3.13`. This is consistent across all of them and has shipped fine — don't "fix" it in one workflow alone.
- The publish job pins **Node 22** on purpose: Node 24 / npm 11 can make vsce's `npm list` detection fail with `ELSPROBLEMS` during packaging. `setup-bun` provides no Node runtime, and the publish scripts and `npx ovsx` need one.
- `gh run watch --exit-status` has returned exit 0 on a failed run. Always confirm with `gh run view <id> --json status,conclusion` before acting on a result.
- `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; no publish workflow has an untrusted trigger surface.
- Environment-approval runs left waiting don't time out quickly — they sit for days and block their tag's concurrency group.
- Open VSX has held a first-time publish in moderation before (logs say "Published", API 404s for hours). Verify with the API query rather than the log line.
- 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.
-6
View File
@@ -319,12 +319,6 @@ jobs:
# 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 }}
# The DMG background, window size, and icon positions from
# tauri.conf.json are applied by a Finder AppleScript in Tauri's
# bundle_dmg.sh. When CI=true (always set on Actions) the bundler
# passes --skip-jenkins and silently skips that script, shipping a
# bare DMG. macOS runners have a GUI session, so opt out of the skip.
TAURI_BUNDLER_DMG_IGNORE_CI: "true"
# 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
+32
View File
@@ -9,6 +9,9 @@ on:
- "apps/examples/desktop-app/package.json"
- "apps/examples/desktop-app/scripts/dmg-background.ts"
- "apps/examples/desktop-app/scripts/dmg-background.test.ts"
- "apps/examples/desktop-app/scripts/build-sidecar-bin.ts"
- "apps/examples/desktop-app/scripts/bun-cross-compile-runtime.ts"
- "apps/examples/desktop-app/scripts/bun-cross-compile-runtime.test.ts"
- "apps/examples/desktop-app/src-tauri/dmg/background.png"
- "apps/examples/desktop-app/src-tauri/dmg/background@2x.png"
- ".github/workflows/desktop-test.yml"
@@ -20,6 +23,9 @@ on:
- "apps/examples/desktop-app/package.json"
- "apps/examples/desktop-app/scripts/dmg-background.ts"
- "apps/examples/desktop-app/scripts/dmg-background.test.ts"
- "apps/examples/desktop-app/scripts/build-sidecar-bin.ts"
- "apps/examples/desktop-app/scripts/bun-cross-compile-runtime.ts"
- "apps/examples/desktop-app/scripts/bun-cross-compile-runtime.test.ts"
- "apps/examples/desktop-app/src-tauri/dmg/background.png"
- "apps/examples/desktop-app/src-tauri/dmg/background@2x.png"
- ".github/workflows/desktop-test.yml"
@@ -48,3 +54,29 @@ jobs:
# not need a workspace dependency install or macOS runner.
- name: Test DMG background tooling
run: bun run test:dmg-background
windows-linux-cross-compile:
name: Test Windows to Linux Bun cross-compilation
runs-on: windows-latest
defaults:
run:
working-directory: apps/examples/desktop-app
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Test runtime selection and validation
run: bun run test:bun-cross-compile-runtime
- name: Prepare pinned Linux runtimes
run: bun scripts/bun-cross-compile-runtime.ts bun-linux-x64 bun-linux-arm64
- name: Cross-compile Linux smoke executables
run: |
bun build scripts/bun-cross-compile-runtime.ts --compile --target=bun-linux-x64 --outfile "$env:RUNNER_TEMP/runtime-smoke-x64"
bun build scripts/bun-cross-compile-runtime.ts --compile --target=bun-linux-arm64 --outfile "$env:RUNNER_TEMP/runtime-smoke-arm64"
+2 -10
View File
@@ -6,13 +6,13 @@ on:
- main
paths:
- "sdk/**"
- "apps/examples/desktop-app/**"
- ".github/workflows/sdk-test.yml"
workflow_dispatch:
pull_request:
branches:
- main
paths:
- "sdk/**"
- "apps/examples/desktop-app/**"
- ".github/workflows/sdk-test.yml"
workflow_call:
@@ -98,14 +98,6 @@ jobs:
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'windows-latest' }}
run: bun -F './sdk/packages/**' test
- name: Run Desktop Sidecar Tests
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && matrix.os == 'ubuntu-latest' }}
run: bun -F @cline/code test:sidecar
- name: Run Desktop Settings UI Tests
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' }}
run: bun -F @cline/code test:settings-ui
- name: Smoke test SQLite under Node
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && matrix.os != 'windows-latest' }}
timeout-minutes: 10
+2 -16
View File
@@ -61,9 +61,8 @@ Emission ownership:
- `user.extension_activated`: emitted **once per host process** by host-specific helpers
(`captureCliExtensionActivated` for the CLI, `captureExtensionActivated` for VS Code).
- `workspace.initialized` / `workspace.init_error`: emitted by a de-duplicated emitter in
`prepareLocalRuntimeBootstrap`. A dedicated host emits once per workspace; a shared Hub emits
once per client surface and workspace. Hosts must NOT re-emit these.
- `workspace.initialized` / `workspace.init_error`: emitted by a per-process de-duplicated
emitter in `prepareLocalRuntimeBootstrap`. Hosts must NOT re-emit these.
- `workspace.path_resolved`: emitted from default tool executors **only when**
`WorkspaceManager` exposes more than one root.
- `task.*`: emitted by core session lifecycle code in `sdk/packages/core/src/cline-core/` and
@@ -105,19 +104,6 @@ hub-backed session, so the daemon must own its own `ITelemetryService`. It build
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.
The Hub transport forwards the serializable `ExtensionContext.client` and
`ExtensionContext.user` values with session create/restore requests. The daemon wraps its
process-owned service with `createClientScopedTelemetryService()` so lifecycle events use the
originating client's `cline_type`, platform/version, and current account/organization without
mutating the singleton shared by concurrent clients. Keep canonical task fields named
`provider` and `model`; do not reintroduce host-specific aliases such as `apiProvider` or
`modelId` for `task.created`, `task.restarted`, or `task.completed`.
`UserContext.distinctId` may be an anonymous machine ID. Set `UserContext.accountId` to the
authenticated account ID (or `null` for an explicitly signed-out client) whenever a client
forwards user context; this prevents machine IDs and stale daemon identity from becoming
`user_id` / `organization_id` on task events.
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).
-36
View File
@@ -1,41 +1,5 @@
# Changelog
## [4.1.18]
This is the first release built solely from the SDK extension. Earlier 4.1.x releases shipped a combined package containing both the SDK extension and the previous one, with a rollout flag choosing between them per window; that migration is now complete, so the package contains only the SDK extension and is substantially smaller to download and install.
### Added
- Images attached to a model that cannot read them are now flagged instead of silently discarded. Thumbnails get a warning badge and the composer explains that the images will be ignored, with a button to switch to an image-capable model. Previously the thumbnail looked normal and the image was replaced with a text placeholder just before the request, so there was no way to tell it had been dropped. Model info and the attachment picker also report image support accurately for models that declare text-only input without listing capabilities.
### Fixed
- A model turn that fails mid-stream with a transient provider error is now retried up to three times with backoff instead of ending the task. A single rate-limit response forwarded by a gateway previously surfaced as a failed task. A turn that has already streamed output is never retried, so nothing is duplicated.
- Terminal commands that succeed without printing anything (`git add -A` on a clean tree, for example) are now reported as empty output. They were treated as a shell-integration failure, which fed the model a snapshot of unrelated terminal scrollback prefixed with a warning that the output could not be captured, so silent commands intermittently looked like failures.
- Checkpoints no longer re-hash every untracked file on each message. In workspaces holding large untracked directories this delayed every message by seconds to minutes; a persistent per-task index now lets git skip files it has already seen.
- `run_commands` no longer hangs until its timeout after a command that backgrounds a child process. The command had finished, but the backgrounded process held the output pipes open.
- `apply_patch` no longer silently overwrites an existing file when the model uses "Add File" on a path that already exists. The file's contents were replaced with no error and no record of what was lost.
- PowerShell commands the model wrapped in another `powershell -Command "..."` are no longer parsed twice. The outer shell consumed `$_` before the inner command ran, so pipelines using it emitted an error for every item processed while still reporting success.
- Opening your home directory as a workspace no longer drives the extension host to exhaustion. Typing an `@` mention indexed every file beneath it and re-ranked the whole index on each keystroke.
- The "Supports Images" checkbox in OpenAI Compatible model settings now stays where you put it. The checkbox rendered the last committed value, and the re-sync that arrived during the save round-trip was re-emitted as a change event that wrote the old value back over your edit.
- The error shown to the model when an `editor` call omits the text to replace now names the file and explains how to recover. The previous message was terse enough that some models re-sent the identical call until the task stopped.
- Credentials are now stripped of invisible characters when saved, not only when pasted, and the cleanup covers AWS, GCP, and SAP fields and custom header values in addition to API keys.
- Cline Pass now defaults to a model from your subscription rather than a free one. Its model list contains both tiers and the default was whichever model was published most recently, so a subscriber who never picked a model could be left on the free tier.
- Cline Pass and free models now show no cost rather than the underlying market price, which is not what you are billed for.
- Model pickers now fall back to the full Recommended, Free, and Subscribed lists when the models endpoint cannot be reached. The offline fallback was a short hardcoded list with no subscription tier at all.
- Model lists for providers sharing the built-in catalog now refresh from the live catalog, so newly published models appear without an extension update, with timeouts so an unresponsive provider endpoint cannot stall the list.
- Claude Code and OpenCode no longer ask for an API key they never read. Both authenticate from their own local CLI's credentials, but they were treated as key-based providers; the workaround was storing a dummy key. A missing CLI on `PATH` now warns rather than blocking, since a configured path or bundled binary also works.
- The OpenAI Codex (ChatGPT subscription) model list no longer offers models the backend rejects, and Codex context limits are applied to every Codex model instead of being inherited from the OpenAI API catalog, which inflated both the context budget and the usage figures derived from it.
- Models served by OpenCode Go are now sent over the wire protocol each one actually speaks. Every model was sent over the OpenAI chat-completions adapter, so models on that endpoint speaking other protocols failed or misbehaved.
- Task history no longer goes blank when a task spawns many subagents. Subagent rows crowded out the tasks that created them, hiding the parent task and everything older.
- Langfuse tracing, when configured, is now limited to the Cline and Cline Pass providers. The provider was ignored, so prompts and responses sent to third-party and bring-your-own-key providers were exported too.
### Changed
- Web search is now enabled by default on models that support it, outside YOLO mode. It can still be turned off in settings, and a settings file that cannot be read leaves it disabled rather than silently on.
- The `run_commands` tool description now names the PowerShell edition in use — `Windows PowerShell (powershell.exe)` versus `PowerShell (pwsh.exe)` — quotes its guidance against that executable, and tells the model to run commands directly rather than wrapping them in another shell invocation. It also no longer describes the environment as Windows when `pwsh` is the configured shell on macOS or Linux.
- Refreshed the built-in model catalog. Adds four providers (Infer by Flow7, Melious, NaN, and Wallaby) and takes the catalog from 5,788 to 6,079 models. This is a wide refresh: the resolved default model changes for 44 providers, most of them landing on DeepSeek V4.1 Flash — among them Hugging Face, Fireworks, Requesty, Nebius, Cortecs, CrossModel, DigitalOcean, Eden AI, and OpenCode Go. Gemini and Vertex now resolve to Gemini 3.8 Flash, GitHub Copilot and Vivgrid to GPT-6 Astra, and NVIDIA to GLM 5.3 Flash. If you use a provider without pinning a model, expect a different default.
## [4.1.17]
Everything here lands through the SDK bundle, so it applies to windows running that bundle.
+14 -10
View File
@@ -5,7 +5,7 @@
<h1 align="center">Cline</h1>
<p align="center">
The open source coding agent in your IDE, terminal, and desktop.
The open source coding agent in your IDE and terminal.
</p>
<div align="center">
@@ -44,7 +44,7 @@ The open source coding agent in your IDE, terminal, and desktop.
### CLI
Run Cline in your terminal.
Interactive chat or fully headless
Interactive chat or fully headless
for CI/CD and scripting.
```
@@ -57,13 +57,17 @@ npm i -g cline
</td>
<td align="center" width="50%">
### Desktop App
### Kanban
Cline as a native app for macOS and Windows.
Run agent sessions in any folder, schedule
routines, and manage models, plugins, and MCP servers.
Run many agents in parallel from a
web-based task board. Each card gets its own
worktree, auto-commit, and dependency chains.
<a href="https://cline.bot/desktop">Download for macOS and Windows</a>
```
npm i -g kanban
```
<a href="https://github.com/cline/kanban">Learn more</a>
<br><br>
</td>
@@ -104,7 +108,7 @@ the JetBrains family.
### SDK
Build your own AI agents and integrations powered by the same engine that runs the CLI, desktop app, VS Code extension, and JetBrains plugin. Custom tools, multi-agent teams, connectors, scheduled automations, and more.
Build your own AI agents and integrations powered by the same engine that runs the CLI, Kanban, VS Code extension, and JetBrains plugin. Custom tools, multi-agent teams, connectors, scheduled automations, and more.
```
npm install @cline/sdk
@@ -127,8 +131,8 @@ npm install @cline/sdk
| **SDK** | Node.js programmatic agent API and extension exports. | [`sdk/`](https://github.com/cline/cline/tree/main/sdk) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/sdk/CHANGELOG.md) |
| **CLI** | Terminal UI, headless mode, shell commands, and CLI-specific flows. | [`apps/cli/`](https://github.com/cline/cline/tree/main/apps/cli) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/apps/cli/CHANGELOG.md) |
| **VS Code Extension** | The Marketplace extension and extension host integration. | [`/`](https://github.com/cline/cline/tree/main) (WIP migrating) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/CHANGELOG.md) |
| **Desktop App** | Native macOS and Windows app (Tauri shell, Bun sidecar, Next.js UI). | [`apps/examples/desktop-app/`](https://github.com/cline/cline/tree/main/apps/examples/desktop-app) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/apps/examples/desktop-app/CHANGELOG.md) |
| **JetBrains Plugin** | JetBrains-hosted client that talks to the shared agent core. | Currently we are not open-sourcing JetBrains plugins | - |
| **Kanban** | Web-based multi-agent task board. | [`cline/kanban`](https://github.com/cline/kanban) | [CHANGELOG.md](https://github.com/cline/kanban/blob/main/CHANGELOG.md) |
| **Docs site** | Public documentation pages. | [`docs/`](https://docs.cline.bot/) | - |
## Edits Code Across Your Project
@@ -182,7 +186,7 @@ const deployTool = createTool({
const agent = new Agent({ tools: [deployTool], /* ... */ })
```
...or use [MCPs](https://github.com/modelcontextprotocol) to connect to databases, query APIs, manage cloud infrastructure, and interact with external systems. Use [community-built servers](https://github.com/modelcontextprotocol/servers) or ask Cline to create custom tools on the fly. In the CLI, manage servers with `cline mcp`.
...or use [MCP servers](https://github.com/modelcontextprotocol) to connect to databases, query APIs, manage cloud infrastructure, and interact with external systems. Use [community-built servers](https://github.com/modelcontextprotocol/servers) or ask Cline to create custom tools on the fly. In the CLI, manage servers with `cline mcp`.
## Multi-Agent Teams
-28
View File
@@ -1,33 +1,5 @@
# Cline CLI Changelog
## 3.0.62
- Introducing Cline Desktop: a native app for working with open-weight models, with task import from Claude Code and Codex, scheduled runs, web search and voice input, and a marketplace for plugins, MCP servers, and skills. The CLI now shows a one-time startup notice pointing at cline.bot/desktop. At most one notice appears per launch, and `CLINE_DISABLE_CLINE_PASS_NOTICE=1` still suppresses all of them
- Agent Plugins are now managed through the Hub. Packages under `~/.agents/plugins/*` are discovered and validated, their skills are exposed through the skills tool as `plugin-name:skill-name`, and their MCP servers start without touching `cline_mcp_settings.json`. The config screen lists them separately from Cline Plugins and Space toggles them. Workspace `.agents/plugins` directories are deliberately not scanned, so opening a repo cannot implicitly start repo-controlled MCP servers
- A model turn that dies mid-stream with a transient provider error is now retried up to 3 times with backoff instead of failing the run — a single forwarded 429 from OpenRouter previously ended the run with exit 1. A turn that already streamed output is never retried, so nothing is duplicated
- Streaming output is no longer throttled by the Hub. Every streamed token was proxied to client hooks as a round trip carrying a full copy of the session, with the agent loop waiting on it
- Checkpoints no longer stall every message in workspaces with large untracked directories. Each turn re-hashed every untracked file before the model call, which on multi-GB workspaces blocked messages for seconds to minutes; a persistent per-session snapshot index now lets git skip unchanged files
- Fixed `run_commands` hanging until timeout after a command that backgrounds a child process. The command had finished, but the backgrounded child held the output pipes open
- Fixed `cline` being OOM-killed when run from your home directory. Typing an `@` mention indexed every file under `$HOME` and re-ranked it on each keystroke
- Fixed `apply_patch` silently overwriting an existing file when the model used "Add File" on a path that already exists
- Fixed PowerShell commands that the model wrapped in another `powershell -Command "..."` being parsed twice, which stripped `$_` out of pipelines and produced an error per enumerated item while still reporting success. The tool description now also names which PowerShell edition is in use and tells the model not to wrap commands
- API keys pasted with an invisible character (BOM, zero-width space) are now cleaned before being saved. They were stored corrupted and the provider's 401 was indistinguishable from a wrong key
- Signing in with Claude Code no longer demands an API key it never reads. It authenticates from the `claude` CLI's own credential store, but onboarding treated it as an API-key provider and dropped you into the sign-in wizard; the workaround was storing a dummy key. A missing `claude` on PATH now warns instead of blocking, since a configured path, bundled binary, or npx also works. OpenCode gets the same local-CLI treatment
- Web search is now on by default in non-yolo sessions on models that support it
- A running Hub on the same core version as your CLI no longer prompts you to update it. Anyone with both the desktop app and the CLI installed saw a "Cline Hub was updated" dialog on every launch that could never resolve
- Scheduled runs no longer stall behind each other — one long turn blocked dispatch of every other schedule. Parallelism limits are now enforced at claim time, work resumed after system sleep is no longer started twice, and a schedule created without a timezone uses your local one instead of an implicit default
- Session history no longer goes blank when a session spawns many subagents. Child rows crowded out the roots, hiding the parent session and everything older
- Fixed TUI toasts being clipped to their first line. The Hub messages that use them are all longer than that, so the keep-Hub reminder never showed the `cline hub upgrade` command it exists to deliver
- Cline Pass now defaults to a subscribed model instead of a free one. Its model list mixes both tiers and the default was whichever model shipped most recently, so subscribers who never picked a model were put on the free tier
- Cline Pass and free models now show zero cost instead of the upstream market price for requests you are not billed per token for
- Model pickers now fall back to the full Recommended, Free, and Subscribed tiers when the models endpoint is unreachable. Previously the offline fallback was six hardcoded models with no subscribed tier at all
- The OpenAI Codex (ChatGPT subscription) model list no longer includes models the backend rejects, and Codex context limits are applied to every Codex model instead of being inherited from the OpenAI API catalog, which was inflating the context budget and the usage math
- Model lists for all shared-catalog providers now refresh from the live catalog, so newly published models appear without a CLI update, with timeouts so a hung provider endpoint cannot stall the list
- Cline Pass now shows as a configured provider after sign-in — it stores credentials under Cline, so one sign-in configured both but only Cline appeared
- Fixed OpenCode Go serving several wire protocols behind one URL while every model was sent over the OpenAI chat-completions adapter
- Collapsed a nested `undici@5.29.0` (CVE-2026-1525) onto 7.x. The earlier remediation's version-scoped override key was silently ignored by Bun, so a vulnerable copy survived
- Refreshed the model catalog. Adds four providers (Infer by Flow7, Melious, NaN, and Wallaby) and takes the bundled catalog from 5,788 to 6,079 models. This is a wide refresh: the resolved default model changes for 44 providers, most of them landing on DeepSeek V4.1 Flash — among them Hugging Face, Fireworks, Requesty, Nebius, Cortecs, CrossModel, DigitalOcean, Eden AI, and OpenCode Go. Gemini and Vertex now resolve to Gemini 3.8 Flash, GitHub Copilot and Vivgrid to GPT-6 Astra, and NVIDIA to GLM 5.3 Flash. If you use any provider without pinning a model, expect a different default
## 3.0.61
- Cline now handles a running Hub that is older than your CLI. Instead of quietly talking to a hub executing stale code, you get a prompt showing how many active sessions a replacement would interrupt, with enter-to-replace or escape-to-keep. The replacement drains the Hub first so in-flight turns finish, and a hub too old or wedged to accept the drain is left alone rather than killed
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cline/cli",
"displayName": "cline",
"version": "3.0.62",
"version": "3.0.61",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"type": "module",
"publishConfig": {
-3
View File
@@ -32,9 +32,6 @@ const BUILD_TIME_INLINED_ENV_VARS = [
"OTEL_TELEMETRY_ENABLED",
"OTEL_LOGS_EXPORTER",
"OTEL_METRICS_EXPORTER",
"OTEL_TRACES_EXPORTER",
"CLINE_TRACE_SAMPLE_PERCENT",
"CLINE_TRACE_RECORD_CONTENT",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
+1 -2
View File
@@ -2,8 +2,7 @@ import {
createDiscordAdapter,
type DiscordAdapter,
} from "@chat-adapter/discord";
// Note: discord.js@14 declares undici ^6.27.0, but the root package.json
// override ("undici": ">=7.29.0 <8") forces undici 7.x for CVE-2026-1525.
// TODO: Remove the root Undici 6 override when discord.js no longer requires Undici ^6.27.0.
import type { ChatStartSessionRequest } from "@cline/core";
import {
createUserInstructionConfigService,
+18 -16
View File
@@ -1,17 +1,18 @@
// @jsxImportSource @opentui/react
import type { ChoiceContext } from "@opentui-ui/dialog";
import { useDialogKeyboard } from "@opentui-ui/dialog/react";
import { useCallback, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { useDialogPalette } from "../tui/hooks/use-theme";
import {
type DialogDismissKey,
isAnyKeyDismiss,
} from "../tui/utils/dialog-keys";
import { getCliSubscriptionUrl } from "../utils/cline-pass-errors";
import open from "../utils/open";
import type { CliMigrationNotice } from "./notice";
/**
* Enter opens the notice's page; any other (unmodified) key dismisses the
* Enter opens the subscription page; any other (unmodified) key dismisses the
* dialog; modifier-held keys are ignored.
*
* The dialog used to be dismissible only with Esc, but Esc is the least
@@ -19,7 +20,7 @@ import type { CliMigrationNotice } from "./notice";
* timeout disambiguation, and Windows console input layers are known to
* swallow it), which left users stuck behind the promo with no way out.
* Modifier-held keys are ignored so that holding Cmd/Ctrl to click the
* link never dismisses the dialog mid-click.
* subscription link never dismisses the dialog mid-click.
*/
export function resolveMigrationNoticeKeyAction(
key: DialogDismissKey,
@@ -35,26 +36,27 @@ export function MigrationNoticeContent(
) {
const { dialogId, notice, resolve } = props;
const palette = useDialogPalette();
const subscriptionUrl = useMemo(() => getCliSubscriptionUrl(), []);
const [status, setStatus] = useState<string | undefined>();
const openNoticePage = useCallback(() => {
setStatus("Opening in your browser...");
void open(notice.url, { wait: false })
const openSubscriptionPage = useCallback(() => {
setStatus("Opening ClinePass in your browser...");
void open(subscriptionUrl, { wait: false })
.then(() => {
setStatus("Opened in your browser.");
setStatus("Opened ClinePass in your browser.");
})
.catch(() => {
setStatus(
"Could not open the browser automatically. Use the URL below.",
);
});
}, [notice.url]);
}, [subscriptionUrl]);
useDialogKeyboard((key) => {
const action = resolveMigrationNoticeKeyAction(key);
if (action === "ignore") return;
if (action === "open") {
openNoticePage();
openSubscriptionPage();
return;
}
resolve(true);
@@ -64,20 +66,20 @@ export function MigrationNoticeContent(
<box flexDirection="column" paddingX={1} gap={1}>
<text fg={palette.act}>{notice.title}</text>
<box flexDirection="column">
{notice.body.split("\n").map((line) => (
<text key={line} selectable>
{line}
</text>
))}
<text selectable>
ClinePass is a $9.99/month subscription plan to get access to the
latest open-weight coding models with enough quota for day-to-day
work, at a much lower cost than paying API costs directly.
</text>
</box>
<box flexDirection="row">
<text fg={palette.act} selectable>
<a href={notice.url}>{notice.url}</a>
<a href={subscriptionUrl}>{subscriptionUrl}</a>
</text>
</box>
<box flexDirection="row">
<box paddingX={1} backgroundColor={palette.act}>
<text fg={palette.textOnSelection}>{notice.openLabel}</text>
<text fg={palette.textOnSelection}>Open ClinePass</text>
</box>
</box>
{status && <text fg={palette.muted}>{status}</text>}
+5 -38
View File
@@ -55,42 +55,11 @@ describe("migration notice", () => {
);
});
it("does not show after every notice is marked as shown", () => {
const dataDir = createTempDataDir();
markClineCliMigrationNoticeShown(dataDir);
markClineCliMigrationNoticeShown(dataDir, "cline-cli-desktop-launch");
expect(getClineCliMigrationNotice(dataDir)).toBeUndefined();
});
it("shows the desktop launch notice once the ClinePass intro was shown", () => {
it("does not show after the notice is marked as shown", () => {
const dataDir = createTempDataDir();
markClineCliMigrationNoticeShown(dataDir);
const notice = getClineCliMigrationNotice(dataDir);
expect(notice?.id).toBe("cline-cli-desktop-launch");
expect(notice?.url).toBe("https://cline.bot/desktop");
});
it("shows only one notice per launch", () => {
const dataDir = createTempDataDir();
expect(getClineCliMigrationNotice(dataDir)?.id).toBe(
"cline-cli-cline-pass-intro",
);
});
it("marks the desktop launch notice as shown by id", () => {
const dataDir = createTempDataDir();
markClineCliMigrationNoticeShown(dataDir);
markClineCliMigrationNoticeShown(dataDir, "cline-cli-desktop-launch");
const rawState = readFileSync(resolveCliNoticeStatePath(dataDir), "utf8");
expect(rawState).toContain('"cline-cli-cline-pass-intro": true');
expect(rawState).toContain('"cline-cli-desktop-launch": true');
expect(getClineCliMigrationNotice(dataDir)).toBeUndefined();
});
@@ -116,7 +85,7 @@ describe("migration notice", () => {
).toBeUndefined();
});
it("does not show the ClinePass intro when ClinePass is already the active provider", () => {
it("does not show when ClinePass is already the active provider", () => {
const dataDir = createTempDataDir();
expect(
@@ -124,8 +93,8 @@ describe("migration notice", () => {
dataDir,
{},
{ activeProviderId: "cline-pass" },
)?.id,
).toBe("cline-cli-desktop-launch");
),
).toBeUndefined();
});
it("suppresses the active ClinePass provider even when the provider id has surrounding whitespace", () => {
@@ -172,8 +141,6 @@ describe("migration notice", () => {
const rawState = readFileSync(resolveCliNoticeStatePath(dataDir), "utf8");
expect(rawState).toContain("cline-cli-cline-pass-intro");
expect(getClineCliMigrationNotice(dataDir)?.id).not.toBe(
"cline-cli-cline-pass-intro",
);
expect(getClineCliMigrationNotice(dataDir)).toBeUndefined();
});
});
+11 -46
View File
@@ -1,54 +1,20 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { resolveClineDataDir } from "@cline/shared/storage";
import { getCliSubscriptionUrl } from "../utils/cline-pass-errors";
export const CLINE_PASS_NOTICE_ID = "cline-cli-cline-pass-intro";
const DESKTOP_NOTICE_ID = "cline-cli-desktop-launch";
const NOTICE_ID = "cline-cli-cline-pass-intro";
const FORCE_NOTICE_ENV = "CLINE_FORCE_CLINE_PASS_NOTICE";
// Historically named for the ClinePass promo; disables every startup notice.
const DISABLE_NOTICE_ENV = "CLINE_DISABLE_CLINE_PASS_NOTICE";
const DESKTOP_APP_URL = "https://cline.bot/desktop";
export interface CliMigrationNotice {
id: string;
title: string;
body: string;
url: string;
openLabel: string;
}
export interface CliMigrationNoticeOptions {
activeProviderId?: string;
}
function getClinePassNotice(): CliMigrationNotice {
return {
id: CLINE_PASS_NOTICE_ID,
title: "Try ClinePass",
body: "ClinePass is a $9.99/month subscription plan to get access to the latest open-weight coding models with enough quota for day-to-day work, at a much lower cost than paying API costs directly.",
url: getCliSubscriptionUrl(),
openLabel: "Open ClinePass",
};
}
function getDesktopNotice(): CliMigrationNotice {
return {
id: DESKTOP_NOTICE_ID,
title: "Introducing Cline Desktop",
body: [
"A native app for working with open weights models. Use it with ClinePass and our free models, or BYOK.",
"- Import tasks from Claude Code and Codex",
"- Run Cline on a regular schedule",
"- Use web search tool and voice input",
"- Browse Marketplace for plugins, MCPs, and skills",
"Available for macOS and Windows.",
].join("\n"),
url: DESKTOP_APP_URL,
openLabel: "Get Cline Desktop",
};
}
interface CliNoticeState {
shown: Record<string, boolean>;
}
@@ -118,33 +84,32 @@ export function getClineCliMigrationNotice(
if (disableNotice && !forceNotice) {
return undefined;
}
// At most one notice per launch, oldest first, so a user who has already
// dismissed the ClinePass intro sees the desktop launch on their next start.
if (
!shouldSuppressClineCliMigrationNoticeForActiveProvider(
shouldSuppressClineCliMigrationNoticeForActiveProvider(
options.activeProviderId,
env,
) &&
(forceNotice || !noticeState.shown[CLINE_PASS_NOTICE_ID])
)
) {
return getClinePassNotice();
return undefined;
}
if (!noticeState.shown[DESKTOP_NOTICE_ID]) {
return getDesktopNotice();
if (noticeState.shown[NOTICE_ID] && !forceNotice) {
return undefined;
}
return undefined;
return {
id: NOTICE_ID,
title: "Try ClinePass",
};
}
export function markClineCliMigrationNoticeShown(
dataDir = resolveClineDataDir(),
noticeId = CLINE_PASS_NOTICE_ID,
): void {
const noticePath = resolveCliNoticeStatePath(dataDir);
const noticeState = readNoticeState(noticePath);
const nextState: CliNoticeState = {
shown: {
...noticeState.shown,
[noticeId]: true,
[NOTICE_ID]: true,
},
};
mkdirSync(dirname(noticePath), { recursive: true, mode: 0o700 });
+2 -14
View File
@@ -165,14 +165,8 @@ vi.mock("./runtime/run-interactive", () => {
vi.mock("./utils/session", () => sessionMocks);
vi.mock("./session/session", () => sessionMocks);
vi.mock("@cline/core", async () => {
// Keep dispatch tests independent of the full SDK runtime import graph.
// Only persisted-settings behavior needs its real implementation here.
const { readGlobalSettings } = await vi.importActual<
typeof import("../../../sdk/packages/core/src/services/global-settings")
>("../../../sdk/packages/core/src/services/global-settings");
return {
readGlobalSettings,
setSdkLogger: vi.fn(),
...(await vi.importActual("@cline/core")),
resolveProviderConfig: llmMocks.resolveProviderConfig,
createTeamName: vi.fn(() => "team-test"),
createUserInstructionConfigService: vi.fn(() => ({
@@ -793,9 +787,6 @@ describe("runCli lightweight command dispatch", () => {
const notice = {
id: "cline-cli-cline-pass-intro",
title: "Try ClinePass",
body: "ClinePass body",
url: "https://app.cline.bot/dashboard/subscription?personal=true",
openLabel: "Open ClinePass",
};
migrationNoticeMocks.getClineCliMigrationNotice.mockReturnValue(notice);
process.argv = ["bun", "src/index.ts"];
@@ -819,7 +810,7 @@ describe("runCli lightweight command dispatch", () => {
await options?.onInitialNoticeShown?.(notice);
expect(
migrationNoticeMocks.markClineCliMigrationNoticeShown,
).toHaveBeenCalledWith(undefined, notice.id);
).toHaveBeenCalledTimes(1);
});
it("passes the active ClinePass provider into the migration notice gate", async () => {
@@ -1180,9 +1171,6 @@ describe("runCli lightweight command dispatch", () => {
migrationNoticeMocks.getClineCliMigrationNotice.mockReturnValue({
id: "cline-cli-cline-pass-intro",
title: "Try ClinePass",
body: "ClinePass body",
url: "https://app.cline.bot/dashboard/subscription?personal=true",
openLabel: "Open ClinePass",
});
process.argv = ["bun", "src/index.ts", "history"];
+2 -2
View File
@@ -1198,8 +1198,8 @@ export async function runCli(): Promise<void> {
activeProviderId: provider,
});
if (initialNotice) {
markInitialNoticeShown = (notice) => {
markClineCliMigrationNoticeShown(undefined, notice.id);
markInitialNoticeShown = () => {
markClineCliMigrationNoticeShown();
};
}
}
+1 -1
View File
@@ -80,8 +80,8 @@ describe("applyInteractiveModelChange", () => {
}));
const saveProviderSettings = vi.fn(() => ({
version: 1 as const,
providers: {},
modes: {},
providers: {},
}));
const ensureReady = vi.fn(async () => {});
const restartWithCurrentMessages = vi.fn(async () => {});
+2 -7
View File
@@ -2,7 +2,7 @@ import {
type BuiltinToolAvailabilityContext,
getCoreBuiltinToolCatalog,
resolveDisabledToolNames,
resolveModelToolSettings,
resolveEnabledOptInToolNames,
type ToolCatalogEntry,
} from "@cline/core";
@@ -11,15 +11,10 @@ export type { ToolCatalogEntry } from "@cline/core";
export function getToolCatalog(
availabilityContext?: BuiltinToolAvailabilityContext,
): ToolCatalogEntry[] {
const modelToolSettings = resolveModelToolSettings();
return getCoreBuiltinToolCatalog({
clientType: "cli",
disabledToolIds: resolveDisabledToolNames(),
enabledModelToolIds: new Set(
Object.entries(modelToolSettings)
.filter(([, setting]) => setting?.enabled === true)
.map(([name]) => name),
),
enabledOptInToolIds: resolveEnabledOptInToolNames(),
...availabilityContext,
});
}
+41 -7
View File
@@ -11,8 +11,6 @@ import {
getValidClineCredentials,
type ProviderSettings,
ProviderSettingsManager,
persistClineAccountTelemetryIdentity,
resolveClineAccountTelemetryIdentity,
saveLocalProviderOAuthCredentials,
type UserCurrentPlan,
} from "@cline/core";
@@ -160,6 +158,38 @@ export async function createClineAccountService(input: {
});
}
/**
* Persist the active organization so headless runs and the hub daemon can
* attach it to telemetry identity. Personal account clears stale org fields.
*/
function persistClineOrganizationContext(
activeOrganization: ClineAccountOrganization | null,
userId: string,
): void {
try {
const manager = new ProviderSettingsManager();
const persisted = manager.getProviderSettings("cline");
if (!persisted) {
return;
}
manager.saveProviderSettings(
{
...persisted,
auth: {
...persisted.auth,
accountId: persisted.auth?.accountId ?? userId,
organizationId: activeOrganization?.organizationId,
organizationName: activeOrganization?.name,
memberId: activeOrganization?.memberId,
},
},
{ setLastUsed: false },
);
} catch {
// Best-effort only.
}
}
export async function loadClineAccountSnapshot(input: {
config: ClineAccountConfig;
clineApiBaseUrl?: string;
@@ -183,12 +213,16 @@ export async function loadClineAccountSnapshot(input: {
const displayedBalance = activeOrganization
? (organizationBalance?.balance ?? balance.balance)
: balance.balance;
const accountContext = resolveClineAccountTelemetryIdentity(user);
const accountContext = {
id: user.id,
email: user.email,
provider: "cline",
organizationId: activeOrganization?.organizationId,
organizationName: activeOrganization?.name,
memberId: activeOrganization?.memberId,
};
identifyTelemetryAccount(accountContext, input.config.logger);
persistClineAccountTelemetryIdentity(
new ProviderSettingsManager(),
accountContext,
);
persistClineOrganizationContext(activeOrganization, user.id);
return {
user,
@@ -14,6 +14,27 @@ export function resolveHubUpdateRequiredKeyAction(
return "ignore";
}
/**
* Human phrase for the live work an outdated Hub is serving, used by the
* "Hub update required" dialog. Falls back to an unquantified phrase when the
* Hub could not answer the activity query.
*/
export function describeOutdatedHubSessions(counts: {
activeSessionCount?: number;
participantClientCount?: number;
}): string {
const sessions = counts.activeSessionCount;
if (typeof sessions !== "number" || sessions <= 0) {
return "active sessions from other Cline clients";
}
const sessionsPhrase = `${sessions} active session${sessions === 1 ? "" : "s"}`;
const clients = counts.participantClientCount;
if (typeof clients !== "number" || clients <= 0) {
return sessionsPhrase;
}
return `${sessionsPhrase} from ${clients} connected Cline client${clients === 1 ? "" : "s"}`;
}
/**
* Yolo and sandbox sessions force the local backend and never attach to the
* shared managed Hub (see the forceLocalBackend condition in the interactive
@@ -1,9 +1,11 @@
// @jsxImportSource @opentui/react
import { describeOutdatedHubSessions } from "@cline/shared";
import type { ChoiceContext } from "@opentui-ui/dialog";
import { useDialogKeyboard } from "@opentui-ui/dialog/react";
import { useDialogPalette } from "../../hooks/use-theme";
import { resolveHubUpdateRequiredKeyAction } from "./hub-update-required-helpers";
import {
describeOutdatedHubSessions,
resolveHubUpdateRequiredKeyAction,
} from "./hub-update-required-helpers";
export interface HubUpdateRequiredDetails {
hubCoreVersion?: string;
@@ -1,7 +1,6 @@
import {
completeClineDeviceAuth,
getProviderConfigFields,
isLocalAuthProvider,
isOAuthProvider,
loginLocalProvider,
type ProviderConfigFieldKey,
@@ -16,10 +15,11 @@ import type { ChoiceContext } from "@opentui-ui/dialog";
import { useDialogKeyboard } from "@opentui-ui/dialog/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
checkLocalCliInstalled,
type LocalCliStatus,
type ProviderLocalCli,
} from "../../../utils/local-cli";
CODEX_CLI_INSTALL_URL,
type CodexCliStatus,
checkCodexCliInstalled,
isOpenAICodexCliProvider,
} from "../../../utils/codex-cli";
import open from "../../../utils/open";
import { listLocalProviders } from "../../../utils/provider-catalog";
import { useDialogPalette } from "../../hooks/use-theme";
@@ -33,7 +33,6 @@ import {
updateProviderConfigValue,
} from "../../utils/provider-config-values";
import { getProviderSection } from "../../utils/provider-sections";
import { canContinueLocalCliSetup } from "../../views/onboarding/model";
import {
getSearchableListRowsWindow,
type SearchableItem,
@@ -83,7 +82,7 @@ export function ProviderPickerContent(
// just a model id and base URL) still render as configured.
isConfigured: p.enabled === true,
isOAuth: isOAuthProvider(p.id),
isLocalAuth: isLocalAuthProvider(p.id),
isLocalAuth: isOpenAICodexCliProvider(p.id),
capabilities: p.capabilities,
}));
setProviders(providerItems);
@@ -655,25 +654,29 @@ export function ProviderConfigInputContent(
);
}
export function LocalCliStatusContent(
export function CodexCliStatusContent(
props: ChoiceContext<boolean> & {
cli?: ProviderLocalCli;
providerName: string;
},
) {
const { resolve, dismiss, dialogId, cli, providerName } = props;
const { resolve, dismiss, dialogId, providerName } = props;
const palette = useDialogPalette();
const [status, setStatus] = useState<LocalCliStatus | undefined>();
const [status, setStatus] = useState<CodexCliStatus | undefined>();
const [checking, setChecking] = useState(false);
const refresh = useCallback(() => {
if (!cli) return;
setStatus(undefined);
setChecking(true);
checkLocalCliInstalled(cli)
checkCodexCliInstalled()
.then(setStatus)
.catch((error: unknown) => {
setStatus({
installed: false,
reason: error instanceof Error ? error.message : String(error),
});
})
.finally(() => setChecking(false));
}, [cli]);
}, []);
useEffect(() => {
refresh();
@@ -688,7 +691,7 @@ export function LocalCliStatusContent(
refresh();
return;
}
if (key.name === "return" && canContinueLocalCliSetup(cli, status)) {
if (key.name === "return" && status?.installed) {
resolve(true);
}
}, dialogId);
@@ -699,37 +702,31 @@ export function LocalCliStatusContent(
<strong>{providerName}</strong>
</text>
{checking && <text fg="gray">Checking for {providerName}...</text>}
{checking && <text fg="gray">Checking for Codex CLI...</text>}
{status?.installed && (
<box flexDirection="column" gap={1}>
<text fg={palette.success}>
{"\u25cf"} {providerName} installed
</text>
<text fg={palette.success}>{"\u25cf"} Codex CLI installed</text>
<text fg="gray">{status.version}</text>
</box>
)}
{status && !status.installed && (
<box flexDirection="column" gap={1}>
<text fg="yellow">{providerName} was not found</text>
<text fg="yellow">Codex CLI was not found</text>
<text fg="gray">{status.reason}</text>
{cli?.docsUrl && (
<box flexDirection="column">
<text fg="gray">Install {providerName} from:</text>
<text fg={palette.act} selectable>
{cli.docsUrl}
</text>
</box>
)}
<text fg="gray">Install Codex CLI from:</text>
<text fg={palette.act} selectable>
{CODEX_CLI_INSTALL_URL}
</text>
</box>
)}
<text fg="gray">
<em>
{cli
{status?.installed
? "Enter to continue, R to recheck, Esc to go back"
: "Enter to continue, Esc to go back"}
: "R to recheck, Esc to go back"}
</em>
</text>
</box>
+1 -4
View File
@@ -23,9 +23,6 @@ export function Toast(props: { toast: ToastState | null }) {
};
const availableWidth = Math.max(1, width - 4);
const maxWidth = Math.min(44, availableWidth);
// Border and horizontal padding take four columns. An explicit width (not
// maxWidth) is what makes the text wrap instead of clipping at the edge.
const boxWidth = Math.min(maxWidth, props.toast.message.length + 4);
const right = width < 32 ? 0 : 2;
const color = variantColor[props.toast.variant];
@@ -35,7 +32,7 @@ export function Toast(props: { toast: ToastState | null }) {
zIndex={100}
top={1}
right={right}
width={boxWidth}
maxWidth={maxWidth}
border
borderStyle="rounded"
borderColor={color}
@@ -55,6 +55,7 @@ function BashOutput(props: { fullText: string; theme: ResolvedTheme }) {
if (!expanded) {
return (
// biome-ignore lint/a11y/noStaticElementInteractions: OpenTUI box is a terminal renderable, not a DOM element; mouse expands optional output.
<box
flexDirection="column"
paddingLeft={2}
@@ -75,6 +76,7 @@ function BashOutput(props: { fullText: string; theme: ResolvedTheme }) {
}
return (
// biome-ignore lint/a11y/noStaticElementInteractions: OpenTUI box is a terminal renderable, not a DOM element; mouse collapses output.
<box
flexDirection="column"
paddingLeft={2}
@@ -160,6 +162,7 @@ function EditOutput(props: {
const diffPalette = props.theme.diff;
return (
// biome-ignore lint/a11y/noStaticElementInteractions: OpenTUI box is a terminal renderable, not a DOM element; mouse toggles the diff.
<box
flexDirection="column"
paddingLeft={2}
@@ -216,6 +219,7 @@ function ApplyPatchOutput(props: {
const diffPalette = props.theme.diff;
return (
// biome-ignore lint/a11y/noStaticElementInteractions: OpenTUI box is a terminal renderable, not a DOM element; mouse toggles the diff.
<box
flexDirection="column"
paddingLeft={2}
@@ -263,6 +267,7 @@ function GenericOutput(props: { outputSummary: string; fullText?: string }) {
: displayText;
return (
// biome-ignore lint/a11y/noStaticElementInteractions: OpenTUI box is a terminal renderable, not a DOM element; mouse expands long output.
<box
flexDirection="column"
paddingLeft={2}
@@ -278,6 +283,7 @@ function GenericOutput(props: { outputSummary: string; fullText?: string }) {
}
return (
// biome-ignore lint/a11y/noStaticElementInteractions: OpenTUI box is a terminal renderable, not a DOM element; mouse collapses output.
<box
flexDirection="column"
paddingLeft={2}
@@ -304,6 +310,7 @@ export function ToolOutput(props: ToolOutputProps) {
const showDetail =
errorExpanded && presentation.detail !== presentation.summary.trim();
return (
// biome-ignore lint/a11y/noStaticElementInteractions: OpenTUI box is a terminal renderable, not a DOM element; mouse toggles error details.
<box
flexDirection="column"
paddingLeft={2}
+4 -12
View File
@@ -10,7 +10,7 @@ import { isClineProvider } from "@cline/shared";
import type { ChoiceContext } from "@opentui-ui/dialog";
import type { DialogActions } from "@opentui-ui/dialog/react";
import { useCallback } from "react";
import { getLocalCliInfo } from "../../utils/local-cli";
import { isOpenAICodexCliProvider } from "../../utils/codex-cli";
import {
getPersistedProviderApiKey,
isOAuthProvider,
@@ -20,8 +20,8 @@ import type { Config } from "../../utils/types";
import { withLoadingDialog } from "../components/dialogs/loading-dialog";
import {
ClinePassSubscriptionContent,
CodexCliStatusContent,
type ExistingProviderOption,
LocalCliStatusContent,
OAuthApiKeyInputContent,
OAuthLoginContent,
type OAuthLoginResult,
@@ -43,7 +43,6 @@ import {
type ThinkingLevel,
ThinkingLevelContent,
} from "../components/model-selector/model-selector";
import { resolveProviderSetupRoute } from "../views/onboarding/model";
export interface OpenModelSelectorOptions {
onCancel?: () => Promise<void> | void;
@@ -179,9 +178,6 @@ async function runProviderChange(
async () => await getProviderDisplayName(newProviderId),
);
const existingSettings = manager.getProviderSettings(newProviderId);
const needsLocalCliSetup =
resolveProviderSetupRoute(newProviderId) === "local_cli";
const localCliProvider = getLocalCliInfo(newProviderId);
// Manual API key entry is the escape hatch for when OAuth login isn't
// working; only the Cline providers accept a dashboard API key.
@@ -250,16 +246,12 @@ async function runProviderChange(
loginResult === "use_api_key"
? await openManualApiKeyDialog()
: loginResult;
} else if (needsLocalCliSetup) {
} else if (isOpenAICodexCliProvider(newProviderId)) {
saved = await dialog.choice<boolean>({
style: { maxHeight: termHeight - 2 },
closeOnEscape: false,
content: (ctx: ChoiceContext<boolean>) => (
<LocalCliStatusContent
{...ctx}
cli={localCliProvider}
providerName={displayName}
/>
<CodexCliStatusContent {...ctx} providerName={displayName} />
),
});
if (saved) {
+1 -5
View File
@@ -15,10 +15,7 @@ import {
useDialogState,
} from "@opentui-ui/dialog/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
CLINE_PASS_NOTICE_ID,
shouldSuppressClineCliMigrationNoticeForActiveProvider,
} from "../kanban-migration/notice";
import { shouldSuppressClineCliMigrationNoticeForActiveProvider } from "../kanban-migration/notice";
import { MigrationNoticeContent } from "../kanban-migration/notice-dialog";
import {
isSameRepoStatus,
@@ -555,7 +552,6 @@ function App(props: TuiProps) {
if (initialNoticeShownRef.current) return;
if (appView !== "home") return;
if (
notice.id === CLINE_PASS_NOTICE_ID &&
shouldSuppressClineCliMigrationNoticeForActiveProvider(currentProviderId)
) {
initialNoticeShownRef.current = true;
+31 -54
View File
@@ -17,11 +17,10 @@ import {
getIndividualPlanFeatures,
} from "../../../utils/cline-pass-errors";
import {
checkLocalCliInstalled,
getLocalCliInfo,
type LocalCliStatus,
type ProviderLocalCli,
} from "../../../utils/local-cli";
type CodexCliStatus,
checkCodexCliInstalled,
isOpenAICodexCliProvider,
} from "../../../utils/codex-cli";
import open from "../../../utils/open";
import { getPersistedProviderApiKey } from "../../../utils/provider-auth";
import { listLocalProviders } from "../../../utils/provider-catalog";
@@ -60,7 +59,6 @@ import { useOnboardingKeyboard } from "./keyboard";
import {
CLINE_PASS_SUBSCRIPTION_OPTIONS,
type ClinePassSubscriptionStatus,
canContinueLocalCliSetup,
DEFAULT_THINKING_LEVEL_INDEX,
getMainMenuOptions,
type ModelEntry,
@@ -68,7 +66,6 @@ import {
type OnboardingStep,
type ProviderEntry,
type ReasoningEffort,
resolveProviderSetupRoute,
shouldUseFeaturedClineModelPicker,
type ThinkingLevel,
toModelEntriesFromKnownModels,
@@ -106,10 +103,6 @@ export function useOnboardingController(props: OnboardingControllerProps) {
const [authError, setAuthError] = useState("");
const [activeProviderId, setActiveProviderId] = useState("");
const [activeProviderName, setActiveProviderName] = useState("");
const localCli = useMemo(
() => getLocalCliInfo(activeProviderId),
[activeProviderId],
);
const [byoFields, setByoFields] = useState<ProviderConfigFields["fields"]>(
{},
);
@@ -117,11 +110,10 @@ export function useOnboardingController(props: OnboardingControllerProps) {
const [byoValues, setByoValues] = useState<ProviderConfigValues>({});
const [byoFocusedField, setByoFocusedField] =
useState<ProviderConfigFieldKey>("apiKey");
const [localCliStatus, setLocalCliStatus] = useState<
LocalCliStatus | undefined
const [codexCliStatus, setCodexCliStatus] = useState<
CodexCliStatus | undefined
>();
const [localCliChecking, setLocalCliChecking] = useState(false);
const localCliProbeRef = useRef(0);
const [codexCliChecking, setCodexCliChecking] = useState(false);
const authAbortRef = useRef(false);
// Device code flow
@@ -494,23 +486,18 @@ export function useOnboardingController(props: OnboardingControllerProps) {
}
}, [step, clinePassSubscriptionStatus, transitionToModelPicker]);
const refreshLocalCliStatus = useCallback((provider: ProviderLocalCli) => {
// Probing spawns the provider's CLI, so a result can land long after the
// user moved on. Two local-CLI providers share this single status, so an
// unlabelled result could mark the selected provider ready off a probe of
// the previous one (or block it off a stale failure). Only the newest
// probe may write.
const probeId = ++localCliProbeRef.current;
const isCurrentProbe = () => localCliProbeRef.current === probeId;
setLocalCliStatus(undefined);
setLocalCliChecking(true);
checkLocalCliInstalled(provider)
.then((status) => {
if (isCurrentProbe()) setLocalCliStatus(status);
const refreshCodexCliStatus = useCallback(() => {
setCodexCliStatus(undefined);
setCodexCliChecking(true);
checkCodexCliInstalled()
.then(setCodexCliStatus)
.catch((error: unknown) => {
setCodexCliStatus({
installed: false,
reason: error instanceof Error ? error.message : String(error),
});
})
.finally(() => {
if (isCurrentProbe()) setLocalCliChecking(false);
});
.finally(() => setCodexCliChecking(false));
}, []);
const selectProvider = useCallback(
@@ -523,14 +510,12 @@ export function useOnboardingController(props: OnboardingControllerProps) {
}
return;
}
if (resolveProviderSetupRoute(provider.id) === "local_cli") {
if (provider.isLocalAuth || isOpenAICodexCliProvider(provider.id)) {
setActiveProviderId(provider.id);
setActiveProviderName(provider.name);
setStep("local_cli_setup");
// Only providers that name a CLI have something to probe; the
// rest reach the screen with readiness simply unknown.
const localCliProvider = getLocalCliInfo(provider.id);
if (localCliProvider) refreshLocalCliStatus(localCliProvider);
setCodexCliStatus(undefined);
setStep("codex_cli_setup");
refreshCodexCliStatus();
return;
}
const config = getProviderConfigFields(provider.id);
@@ -590,17 +575,11 @@ export function useOnboardingController(props: OnboardingControllerProps) {
setByoFocusedField(firstField ?? "apiKey");
setStep("byo_apikey");
},
[providers, startOAuthFlow, refreshLocalCliStatus, providerSettingsManager],
[providers, startOAuthFlow, refreshCodexCliStatus, providerSettingsManager],
);
const recheckLocalCli = useCallback(() => {
if (localCli) {
refreshLocalCliStatus(localCli);
}
}, [localCli, refreshLocalCliStatus]);
const saveLocalCliConfig = useCallback(() => {
if (!canContinueLocalCliSetup(localCli, localCliStatus)) {
const saveCodexCliConfig = useCallback(() => {
if (!codexCliStatus?.installed) {
return;
}
saveLocalProviderSettings(providerSettingsManager, {
@@ -609,8 +588,7 @@ export function useOnboardingController(props: OnboardingControllerProps) {
transitionToModelPicker(activeProviderId);
}, [
activeProviderId,
localCli,
localCliStatus,
codexCliStatus,
providerSettingsManager,
transitionToModelPicker,
]);
@@ -820,13 +798,13 @@ export function useOnboardingController(props: OnboardingControllerProps) {
deviceAbortRef.current = true;
},
resetAuth,
refreshLocalCliStatus: recheckLocalCli,
refreshCodexCliStatus,
startOAuthFlow,
startDeviceCodeFlow,
selectProvider,
loadModelsForProvider,
saveClineModelSelection,
saveLocalCliConfig,
saveCodexCliConfig,
saveByoConfig,
saveModelSelection,
saveThinkingLevel,
@@ -842,9 +820,8 @@ export function useOnboardingController(props: OnboardingControllerProps) {
byoFields,
byoFocusedField,
byoValues,
localCli,
localCliChecking,
localCliStatus,
codexCliChecking,
codexCliStatus,
clineEntries,
clineModelSelected,
clinePassCurrentPlanName,
@@ -883,7 +860,7 @@ export function useOnboardingController(props: OnboardingControllerProps) {
providersLoading,
recommendedLoading: recommended.loading,
saveByoConfig,
saveLocalCliConfig,
saveCodexCliConfig,
saveCustomModelId,
selectedModelName,
step,
@@ -51,13 +51,13 @@ export function useOnboardingKeyboard(input: {
abortOAuth: () => void;
abortDeviceCode: () => void;
resetAuth: () => void;
refreshLocalCliStatus: () => void;
refreshCodexCliStatus: () => void;
startOAuthFlow: (providerId: OnboardingOAuthProviderId) => void;
startDeviceCodeFlow: (providerId: OnboardingOAuthProviderId) => void;
selectProvider: (providerId: string) => void;
loadModelsForProvider: (providerId: string) => void;
saveClineModelSelection: (modelId: string, modelName: string) => void;
saveLocalCliConfig: () => void;
saveCodexCliConfig: () => void;
saveByoConfig: () => void;
saveModelSelection: () => void;
saveThinkingLevel: (level: ThinkingLevel) => void;
@@ -98,7 +98,7 @@ export function useOnboardingKeyboard(input: {
input.setMenuSelected(0);
return;
}
if (input.step === "local_cli_setup") {
if (input.step === "codex_cli_setup") {
input.setStep("byo_provider");
return;
}
@@ -227,13 +227,13 @@ export function useOnboardingKeyboard(input: {
return;
}
if (input.step === "local_cli_setup") {
if (input.step === "codex_cli_setup") {
if (key.name === "r") {
input.refreshLocalCliStatus();
input.refreshCodexCliStatus();
return;
}
if (key.name === "return") {
input.saveLocalCliConfig();
input.saveCodexCliConfig();
}
return;
}
@@ -1,15 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { getLocalCliInfo } from "../../../utils/local-cli";
vi.mock("../../../utils/local-cli", () => ({
getLocalCliInfo: () => undefined,
}));
import { describe, expect, it } from "vitest";
import {
canContinueLocalCliSetup,
getMainMenuOptions,
getOAuthProviderLabel,
resolveProviderSetupRoute,
shouldUseFeaturedClineModelPicker,
toModelEntriesFromKnownModels,
toModelEntry,
@@ -85,20 +77,6 @@ describe("onboarding model helpers", () => {
});
});
it("marks the Claude Code provider as local auth", () => {
expect(
toProviderEntry({
id: "claude-code",
name: "Claude Code",
models: null,
}),
).toMatchObject({
id: "claude-code",
isOAuth: false,
isLocalAuth: true,
});
});
it("maps model names and reasoning support strictly", () => {
expect(
toModelEntry({
@@ -188,31 +166,3 @@ describe("onboarding model helpers", () => {
expect(shouldUseFeaturedClineModelPicker("anthropic")).toBe(false);
});
});
describe("local-auth setup routing", () => {
// A provider can declare `local-auth` without naming a CLI we can probe.
// Routing must follow the capability; the descriptor is only for probing.
// Otherwise it falls through to the API-key form, which renders no fields
// for a local-auth provider.
it("routes a local-auth provider with no CLI descriptor to local setup", () => {
expect(getLocalCliInfo("claude-code")).toBeUndefined();
expect(resolveProviderSetupRoute("claude-code")).toBe("local_cli");
});
it("routes OAuth and API-key providers unchanged", () => {
expect(resolveProviderSetupRoute("anthropic")).toBe("api_key");
});
// The probe only looks on PATH, while the runtime also accepts an explicit
// pathToClaudeCodeExecutable and a bundled platform binary. A PATH miss
// therefore means "not on PATH", not "unusable", so it must not block.
it("lets the user continue when the CLI is not found on PATH", () => {
const cli = { command: "claude", docsUrl: "https://example.invalid" };
expect(
canContinueLocalCliSetup(cli, {
installed: false,
reason: "The claude executable was not found on PATH.",
}),
).toBe(true);
});
});
+4 -39
View File
@@ -4,14 +4,8 @@ import type {
ModelOperation,
} from "@cline/shared";
import { isChatProviderModel } from "../../../utils/chat-models";
import type {
LocalCliStatus,
ProviderLocalCli,
} from "../../../utils/local-cli";
import {
isLocalAuthProvider,
isOAuthProvider,
} from "../../../utils/provider-auth";
import { isOpenAICodexCliProvider } from "../../../utils/codex-cli";
import { isOAuthProvider } from "../../../utils/provider-auth";
export type OnboardingStep =
| "menu"
@@ -19,7 +13,7 @@ export type OnboardingStep =
| "device_code"
| "byo_provider"
| "byo_apikey"
| "local_cli_setup"
| "codex_cli_setup"
| "cline_pass_subscription"
| "cline_model"
| "model_picker"
@@ -91,35 +85,6 @@ export const MAIN_MENU: MenuOption[] = [
},
];
/**
* Which setup flow a provider needs. Keyed off how the provider authenticates,
* so every caller routes the same way.
*/
export type ProviderSetupRoute = "oauth" | "local_cli" | "api_key";
export function resolveProviderSetupRoute(
providerId: string,
): ProviderSetupRoute {
if (isOAuthProvider(providerId)) return "oauth";
if (isLocalAuthProvider(providerId)) return "local_cli";
return "api_key";
}
/**
* Whether the local-CLI setup screen lets the user connect.
*/
export function canContinueLocalCliSetup(
_cli: ProviderLocalCli | undefined,
_status: LocalCliStatus | undefined,
): boolean {
// The probe only looks on PATH, while the runtime also accepts an explicit
// pathToClaudeCodeExecutable and a bundled platform binary, and Codex falls
// back through `npx`. A PATH miss therefore means "not on PATH", not
// "unusable", so the screen reports it without blocking — a provider that
// really cannot start says so on the first turn, in its own words.
return true;
}
export function getMainMenuOptions(options?: {
isClinePassEnabled?: boolean;
}): MenuOption[] {
@@ -209,7 +174,7 @@ export function toProviderEntry(provider: ProviderCatalogItem): ProviderEntry {
id: provider.id,
name: provider.name,
isOAuth: isOAuthProvider(provider.id),
isLocalAuth: isLocalAuthProvider(provider.id),
isLocalAuth: isOpenAICodexCliProvider(provider.id),
hasAuth:
Boolean(provider.apiKey) || provider.oauthAccessTokenPresent === true,
...(provider.capabilities ? { capabilities: provider.capabilities } : {}),
+15 -24
View File
@@ -2,10 +2,10 @@ import "opentui-spinner/react";
import type { ScrollBoxRenderable } from "@opentui/core";
import type { ReactNode } from "react";
import { useEffect, useRef } from "react";
import type {
LocalCliStatus,
ProviderLocalCli,
} from "../../../utils/local-cli";
import {
CODEX_CLI_INSTALL_URL,
type CodexCliStatus,
} from "../../../utils/codex-cli";
import {
ClineModelPicker,
type ClineModelPickerEntry,
@@ -25,7 +25,6 @@ import { FIELD_ORDER } from "./fields";
import {
type ClinePassSubscriptionOption,
type ClinePassSubscriptionStatus,
canContinueLocalCliSetup,
type MenuOption,
THINKING_LEVELS,
} from "./model";
@@ -363,20 +362,18 @@ export function OnboardingProviderConfigScreen(props: {
);
}
export function OnboardingLocalCliScreen(props: {
export function OnboardingCodexCliScreen(props: {
activeProviderName: string;
checking: boolean;
cli?: ProviderLocalCli;
compact: boolean;
contentWidth: number;
mouse: MouseTrackerState;
status?: LocalCliStatus;
status?: CodexCliStatus;
}) {
const defaultFg = useDefaultFg();
const colors = useOnboardingColors();
const installedStatus =
props.status?.installed === true ? props.status : undefined;
const canContinue = canContinueLocalCliSetup(props.cli, props.status);
return (
<OnboardingFrame
compact={props.compact}
@@ -389,37 +386,31 @@ export function OnboardingLocalCliScreen(props: {
{props.checking && (
<box flexDirection="row" gap={1}>
<spinner name="dots" color="gray" />
<text fg="gray">Checking for {props.activeProviderName}...</text>
<text fg="gray">Checking for Codex CLI...</text>
</box>
)}
{installedStatus && (
<box flexDirection="column" gap={1} alignItems="center">
<text fg={colors.success}>
{"\u25cf"} {props.activeProviderName} installed
</text>
<text fg={colors.success}>{"\u25cf"} Codex CLI installed</text>
<text fg="gray">{installedStatus.version}</text>
</box>
)}
{props.cli && props.status && !props.status.installed && (
{props.status && !props.status.installed && (
<box flexDirection="column" gap={1} width={props.contentWidth}>
<text fg="yellow">{props.activeProviderName} was not found</text>
<text fg="yellow">Codex CLI was not found</text>
<text fg="gray">{props.status.reason}</text>
{props.cli.docsUrl && (
<box flexDirection="column">
<text fg="gray">Install {props.activeProviderName} from:</text>
<text fg={colors.accent} selectable>
{props.cli.docsUrl}
</text>
</box>
)}
<text fg="gray">Install Codex CLI from:</text>
<text fg={colors.accent} selectable>
{CODEX_CLI_INSTALL_URL}
</text>
</box>
)}
<text fg="gray">
<em>
{canContinue
{installedStatus
? "Enter to continue, R to recheck, Esc to go back, Ctrl+C to exit"
: "R to recheck, Esc to go back, Ctrl+C to exit"}
</em>
+5 -6
View File
@@ -7,10 +7,10 @@ import { getOAuthProviderLabel, type OnboardingResult } from "./model";
import {
OnboardingClineModelScreen,
OnboardingClinePassSubscriptionScreen,
OnboardingCodexCliScreen,
OnboardingCustomModelIdScreen,
OnboardingDeviceCodeScreen,
OnboardingDoneScreen,
OnboardingLocalCliScreen,
OnboardingMainMenuScreen,
OnboardingModelPickerScreen,
OnboardingOAuthPendingScreen,
@@ -83,16 +83,15 @@ export function OnboardingView(props: OnboardingViewProps) {
);
}
if (state.step === "local_cli_setup" && state.localCli) {
if (state.step === "codex_cli_setup") {
return (
<OnboardingLocalCliScreen
<OnboardingCodexCliScreen
activeProviderName={state.activeProviderName}
checking={state.localCliChecking}
cli={state.localCli}
checking={state.codexCliChecking}
compact={compact}
contentWidth={contentWidth}
mouse={mouse}
status={state.localCliStatus}
status={state.codexCliStatus}
/>
);
}
+55
View File
@@ -0,0 +1,55 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
export const OPENAI_CODEX_CLI_PROVIDER_ID = "openai-codex-cli";
export const CODEX_CLI_INSTALL_URL = "https://developers.openai.com/codex/cli";
export type CodexCliStatus =
| {
installed: true;
version: string;
}
| {
installed: false;
reason: string;
};
export function isOpenAICodexCliProvider(providerId: string): boolean {
return providerId.trim().toLowerCase() === OPENAI_CODEX_CLI_PROVIDER_ID;
}
export async function checkCodexCliInstalled(): Promise<CodexCliStatus> {
try {
const result = await execFileAsync("codex", ["--version"], {
timeout: 3000,
windowsHide: true,
});
const version = (result.stdout || result.stderr).trim();
return {
installed: true,
version: version || "codex",
};
} catch (error) {
const details =
error && typeof error === "object"
? (error as { code?: unknown; message?: unknown })
: undefined;
const code = typeof details?.code === "string" ? details.code : "";
if (code === "ENOENT") {
return {
installed: false,
reason: "The codex executable was not found on PATH.",
};
}
const message =
typeof details?.message === "string"
? details.message
: "Could not run codex --version.";
return {
installed: false,
reason: message,
};
}
}
-30
View File
@@ -1,30 +0,0 @@
import { isLocalAuthProvider } from "@cline/core";
import { describe, expect, it } from "vitest";
import { getLocalCliInfo } from "./local-cli";
describe("local CLI providers", () => {
it("reads the CLI a local-auth provider borrows credentials from", () => {
expect(getLocalCliInfo("openai-codex-cli")).toEqual({
command: "codex",
docsUrl: "https://developers.openai.com/codex/cli",
});
expect(getLocalCliInfo("claude-code")).toEqual({
command: "claude",
docsUrl: "https://code.claude.com/docs/en/setup",
});
});
it("names no CLI for providers that authenticate with an API key", () => {
expect(getLocalCliInfo("anthropic")).toBeUndefined();
expect(getLocalCliInfo("openai-codex")).toBeUndefined();
});
// Routing is keyed off the capability alone, so a local-auth provider whose
// credentials come from somewhere unprobeable still reaches the local setup
// screen instead of an empty API-key form.
it("routes on the capability, not on knowing a CLI", () => {
expect(isLocalAuthProvider("claude-code")).toBe(true);
expect(isLocalAuthProvider("openai-codex-cli")).toBe(true);
expect(isLocalAuthProvider("anthropic")).toBe(false);
});
});
-59
View File
@@ -1,59 +0,0 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { Llms } from "@cline/core";
const execFileAsync = promisify(execFile);
export type ProviderLocalCli = Llms.ProviderLocalCli;
export type LocalCliStatus =
| {
installed: true;
version: string;
}
| {
installed: false;
reason: string;
};
/**
* The CLI a `local-auth` provider borrows credentials from, as declared in
* the provider catalog. `undefined` for providers that name none those are
* connected without a readiness check rather than probing a guessed command.
*/
export function getLocalCliInfo(
providerId: string,
): ProviderLocalCli | undefined {
return Llms.resolveProviderLocalCli(providerId);
}
export async function checkLocalCliInstalled(
cli: ProviderLocalCli,
): Promise<LocalCliStatus> {
try {
const result = await execFileAsync(cli.command, ["--version"], {
timeout: 3000,
windowsHide: true,
});
const version = (result.stdout || result.stderr).trim();
return {
installed: true,
version: version || cli.command,
};
} catch (error) {
const details = error as NodeJS.ErrnoException | undefined;
if (details?.code === "ENOENT") {
return {
installed: false,
reason: `The ${cli.command} executable was not found on PATH.`,
};
}
return {
installed: false,
reason:
error instanceof Error
? error.message
: `Could not run ${cli.command} --version.`,
};
}
}
+1 -2
View File
@@ -1,7 +1,6 @@
import {
formatProviderOAuthApiKey,
getPersistedProviderApiKey as getCorePersistedProviderApiKey,
isLocalAuthProvider,
isOAuthProvider,
Llms,
type ProviderOAuthCredentials,
@@ -22,7 +21,7 @@ export function normalizeAuthProviderId(providerId: string): string {
return normalizeProviderId(normalized);
}
export { isLocalAuthProvider, isOAuthProvider };
export { isOAuthProvider };
export function toProviderApiKey(
providerId: string,
+7 -1
View File
@@ -1,6 +1,6 @@
import { dirname, join, normalize } from "node:path";
import { fileURLToPath } from "node:url";
import { ProviderSettingsManager } from "@cline/core";
import { ClientSettingsManager, ProviderSettingsManager } from "@cline/core";
import { buildInviteUrl, resolveClineHubServerOptions } from "../options";
import type { BrowserConfig } from "./types";
@@ -19,6 +19,12 @@ export const cliIndexPath = normalize(
);
export const providerSettingsManager = new ProviderSettingsManager();
export const desktopClientSettingsManager = new ClientSettingsManager({
clientId: "desktop",
});
desktopClientSettingsManager.initializeModesIfMissing(
providerSettingsManager.read().modes,
);
export const browserConfig: BrowserConfig = {
inviteRequired: Boolean(roomSecret),
+22 -5
View File
@@ -17,19 +17,25 @@ import {
type ProviderClient,
type ProviderProtocol,
type ProviderSettings,
parseProviderModeSettings,
readGlobalSettings,
saveLocalProviderOAuthCredentials,
saveLocalProviderSettings,
saveModeSettings,
setAutoUpdateEnabledGlobally,
setTelemetryOptOutGlobally,
} from "@cline/core";
import { getClineEnvironmentConfig } from "@cline/shared";
import { getClineEnvironmentConfig, ProviderModeSchema } from "@cline/shared";
import {
connectorChannelsPayload,
startConnectorChannel,
stopConnectorChannel,
} from "./connectors";
import { providerSettingsManager, workspaceRoot } from "./deps";
import {
desktopClientSettingsManager,
providerSettingsManager,
workspaceRoot,
} from "./deps";
import {
installMarketplaceEntryForDesktopCommand,
listMarketplaceInstalledEntries,
@@ -103,15 +109,26 @@ export async function handleDesktopCommand(
await ensureCustomProvidersLoaded(providerSettingsManager);
return await listLocalProviders(providerSettingsManager, {
isClinePassEnabled: true,
modeSettings: desktopClientSettingsManager.read().modes,
});
}
if (command === "list_provider_models") {
const provider = String(args?.provider ?? "").trim();
return await getLocalProviderModels(
provider,
providerSettingsManager.getProviderConfig(provider, {
includeKnownModels: false,
}),
providerSettingsManager.getProviderConfig(provider),
);
}
if (command === "save_mode_settings") {
const mode = ProviderModeSchema.parse(args?.mode);
const settings =
args?.settings == null
? undefined
: parseProviderModeSettings(mode, args.settings);
return await saveModeSettings(
providerSettingsManager,
{ mode, settings },
desktopClientSettingsManager,
);
}
if (command === "save_provider_settings") {
+1 -3
View File
@@ -85,9 +85,7 @@ export async function loadModels(
if (!provider) return;
const payload = await getLocalProviderModels(
provider,
providerSettingsManager.getProviderConfig(provider, {
includeKnownModels: false,
}),
providerSettingsManager.getProviderConfig(provider),
);
const models: WebviewProviderModel[] = payload.models
.filter((model) =>
+15 -3
View File
@@ -5,7 +5,10 @@ import {
createUserInstructionConfigService,
getCoreBuiltinToolCatalog,
listHookConfigFiles,
readGlobalSettings,
ProviderSettingsManager,
resolveConfiguredMediaGenerationTarget,
resolveDisabledToolNames,
resolveEnabledOptInToolNames,
resolveAgentConfigSearchPaths as resolveSharedAgentConfigSearchPaths,
} from "@cline/core";
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
@@ -105,14 +108,23 @@ export async function listUserInstructionConfigs(
cwd: targetWorkspaceRoot,
}),
]);
const disabledTools = new Set(readGlobalSettings().disabledTools ?? []);
const disabledTools = resolveDisabledToolNames();
const mediaGenerationConfigured = Boolean(
await resolveConfiguredMediaGenerationTarget(
new ProviderSettingsManager(),
"image",
),
);
// Pin spawn/teams availability so this listing matches the desktop
// sidecar's (sidecar/commands.ts) even if the preset defaults change.
const builtinToolCatalog = getCoreBuiltinToolCatalog({
enableSpawnAgent: true,
enableAgentTeams: true,
disabledToolIds: disabledTools,
});
enabledOptInToolIds: resolveEnabledOptInToolNames(),
}).filter(
(tool) => tool.id !== "generate_media" || mediaGenerationConfigured,
);
return {
workspaceRoot: targetWorkspaceRoot,
@@ -1,4 +1,8 @@
import type { ModelModality, ModelOperation } from "@cline/shared";
import type {
ModelModality,
ModelOperation,
ProviderModesSettings,
} from "@cline/shared";
export interface ProviderModel {
id: string;
@@ -68,6 +72,7 @@ export interface ProviderSettingsUpdate {
export interface ProviderCatalogResponse {
providers: Provider[];
settingsPath: string;
modes: ProviderModesSettings;
}
export interface ProviderModelsResponse {
+38 -88
View File
@@ -1,96 +1,16 @@
# Cline Desktop Changelog
## 0.0.28
## 0.0.23-beta.1
- The app no longer gets stuck on "Desktop backend unavailable" when the backend is slow to start. The window asked for the backend's address exactly once and the shell stops waiting after about 15 seconds, so on a slower machine — where starting the hub pushed past that — you were left on the error screen until you relaunched, and the relaunch could lose the same race. Connection attempts now keep retrying and re-resolve the address each time, so a backend that has since restarted is reached at its current one
- Confirming text with a Chinese or Japanese IME no longer sends the message. The Enter that commits an in-progress composition also reached the composer, so choosing a candidate fired off a half-typed message; Enter and the arrow keys now belong to the IME while you are composing
- The model picker refreshes its list every time you open it, instead of staying frozen until you restart the app. The Recommended and Free groupings come from a live feed, so a newly promoted free model would otherwise not show up for the rest of the session
- Signing in with Cline now explains what the plan includes. The sign-in card lists the regular free model promotions, ClinePass for generous usage across open-weight models like DeepSeek, Kimi, and GLM, and that no API key is needed; the last onboarding step then shows the current free models alongside a ClinePass summary. This only appears when you sign in with Cline, not when you bring your own API key
- Cline Pass now starts you on a model from your subscription. Its model list carries both subscription and free models and the default was simply the most recently published entry, so a subscriber who never picked a model was left on a free model instead of the tier they pay for
- A turn that fails partway through with a temporary provider error is now retried up to three times instead of failing the task. A single rate-limit response passed along by a provider gateway previously ended the turn outright. A turn that has already produced output is never retried, so nothing is duplicated
- Long responses stream faster. Every chunk of a streaming reply was being handed to client-side hooks as a round trip carrying a full copy of the session, with the agent waiting on each one before continuing
- The command-running tool now tells the model which PowerShell edition it is talking to — `Windows PowerShell (powershell.exe)` versus `PowerShell (pwsh.exe)` — and to write commands directly rather than wrapping them in a second shell invocation, which was corrupting pipelines that use `$_`
- Refreshed the model catalog. Adds three providers (Infer by Flow7, Melious, and Wallaby) and takes the catalog from 5,923 to 6,079 models. The resolved default model changes for 33 providers, most of them landing on DeepSeek V4.1 Flash — among them Hugging Face, Fireworks, Requesty, Cortecs, CrossModel, DigitalOcean, Eden AI, and OpenCode Go — while NVIDIA moves to GLM 5.3 Flash and NanoGPT to GPT Astra. If you use a provider without pinning a model, expect a different default
- Beta: configure and opt in to image generation under Customize → Tools. Provider credentials stay server-side, and generated images remain available in session history.
- Scheduled runs are grouped within their runtime environment so similarly named local and SSH schedules stay separate. While an SSH environment is selected, media-generation settings now make clear that they configure only the local runtime.
- Includes all stable desktop improvements through 0.0.22, including resumable imports from Claude Code, Codex, and opencode; grouped schedule runs; macOS voice input; safer Hub upgrades; and provider and tool compatibility fixes.
## 0.0.27
## 0.0.22-beta.1
- An expired Cline sign-in now produces one actionable error instead of two dead ends. A turn that fails before the runtime takes the prompt was reported twice — the hub's detail-less `run.failed` appended a bubble, then the send RPC resolved with the real error and overwrote the error banner underneath it — so you got a vague bubble stacked on a detailed banner. The failure now renders exactly once, upgrading in place when the detailed report arrives. Credential failures also carry a fix button: **Sign in to Cline** for the Cline provider (Settings → API Providers reads a stale token as "Signed in via browser", so there is nothing to fix there), **Open model settings** for others, and local-CLI providers keep pointing at their CLI. Sessions that fail to start over credentials get the same hint and action
- The Account page now offers a sign-in prompt when your Cline refresh token is rejected, instead of an error card whose **Retry** failed the same way. A rejected refresh was falling back to the persisted access token, which is dead too, so the account request 401'd; a rejected refresh now reports as signed out. Transient refresh failures still fall back
- Signing out of Cline and Cline Pass now sticks. Sign-out removes the provider entry, but the runtime re-imports missing providers from the classic extension's stored credentials on every command, so you appeared signed straight back in — the same root cause as the ChatGPT/Codex fix in 0.0.26, which only cleared Codex secrets. Cline Pass sign-out did nothing at all, because its credentials live under the Cline provider entry rather than its own
- The settings sidebar's **Models** section is now **API Providers**, with an icon to match — it is where you configure providers and their credentials, not where you pick a model
- A failed voice input now shows a "Speech input failed" toast in chat with the actual error, instead of dropping you into Settings → Voice. Any error that wasn't a browser exception was treated as a configuration problem, even though the microphone is already gated on a configured transcription model — so a connection or transcription failure interrupted the chat and hid the real reason. Your draft and the microphone button stay put so you can retry; microphone-permission guidance is unchanged
- OpenCode Go models work again. Requests were missing the `x-opencode-session` header Go requires to route a conversation ("Request is missing x-opencode-session and cannot be routed efficiently"), and every model was sent to `/chat/completions` because catalog normalization discarded the per-model adapter declaration — so Muse Spark, GPT, and Grok models that expect `/responses`, and MiniMax/Qwen models that expect `/messages`, failed with internal server errors. Model-level protocol routing is now preserved end to end, retries reuse the same session identity, and Go Qwen entries that omit the declaration fall back to Messages
- Added Crusoe as an OpenAI-compatible provider
- Settings switches are lighter in light mode and are now a shared native control — real keyboard, label, and form behavior, a guaranteed 24px hit area, and support for reduced-motion and forced-colors
- The welcome screen's eye color matches the dark theme, and its animation no longer triggers layout work each frame
## 0.0.26
- The composer now shows the current branch's GitHub pull request — PR number, merge status, changed-line totals, and CI checks. Click through to open it in your browser, or expand CI to inspect individual checks and their logs; status refreshes every 30 seconds while visible, on window focus, and on demand. If the branch has no PR, **Create PR** opens GitHub's comparison form. Requires the GitHub CLI (`gh`) installed and signed in, plus a GitHub.com `origin` remote; the row hides itself on the default branch, detached HEAD, and unsupported repositories. Cline does not push commits or submit the PR for you
- The Customize view's Tools, Skills, and Rules tabs now read as one consistent list instead of three different ones, matching the pattern Plugins already used. Tools gets a search bar that filters both sections and per-section Enable all/Disable all that only touches what's visible; Skills gets an in-place enable/disable toggle — previously the desktop app had no concept of a disabled skill, so disabled ones were hidden and unreachable — and the same Copy path / Uninstall menu Plugins use. Tabs are reordered to Tools, Plugins, Skills, Rules, MCP, Hooks and open on Tools, and the sidebar's "New Task" button is now "New Session"
- Deleting a queued prompt no longer leaves it in the transcript as a message that never ran. The sidecar inferred a queued prompt had started whenever the pending list shrank and its head changed — which is exactly what deleting the first queued prompt (or discarding the queue) looks like. It now relies only on the runtime's real start event
- A session forked from a checkpoint restore no longer comes back stuck on "Thinking...". Sessions that materialize with seeded history never passed their status when persisting, so the row and manifest were written as "running" while the live session was idle; resuming from that manifest then showed a session that was never going to finish
- Sending an image with no text no longer fails with "session input requires a prompt string" — the composer asks for a message instead of letting the request through to a backend that rejects it
- Image attachments in formats the model pipeline can't read (HEIC, TIFF, SVG, BMP, ICO, and friends) are now rejected at attach time — by picker, paste, or drag-and-drop — instead of failing later in the turn. Files whose type has to be inferred from the extension are classified the same way
- Signing out of ChatGPT (Codex) now sticks. Sign-out removes the provider entry, but the runtime re-imports any missing provider from the classic extension's stored credentials on every command, so the next action signed you straight back in. The legacy Codex credentials are now cleared too, and a failed clear reports the sign-out as failed instead of quietly succeeding
- Auth failures on providers that log in through a local CLI — Claude Code, Codex CLI, OpenCode — now point you at that CLI instead of Settings → Models, where there is nothing to fix. "OAuth session expired", "Not logged in · Please run /login", and similar messages are also recognized as credential failures now, so they get a hint at all
- Model lists now refresh from the live catalog for every provider that uses the shared catalog, not just Cline and Cline Pass. Providers with their own endpoint-owned model lists are unaffected, and the first providers listing is still network-free
- Cline Pass and free models now report zero cost instead of the upstream provider's price
- Session history rows no longer overlap their own hover metadata when a label is wider than the fixed label column
- Scheduled sessions no longer stall out. The poller could stop advancing, and capacity waits were counted as run attempts, so a schedule could burn through its retries without ever running. Execution lifecycle and capacity claims are now fenced atomically. New recurring schedules also default to your local timezone instead of UTC; existing schedules without a timezone keep it that way when edited
- Automation event acceptance is now atomic and retryable, so an event can't be half-accepted and lost if delivery fails mid-way
- Nested PowerShell commands no longer flood errors and look like a hang. Commands run through an outer PowerShell bootstrap, so a nested `powershell -Command "... $_ ..."` had `$_` interpolated away by the outer parser before the inner shell saw it — a `Where-Object { $_.Name ... }` pipeline then errored once per item over a large tree while still exiting 0. Redundant nested invocations are now unwrapped and run directly, only when that provably preserves semantics (same PowerShell edition, no profile loading, fully quoted command). Cline is also told which edition it's actually on — Windows PowerShell vs. Microsoft PowerShell — and to stop wrapping commands in a shell it's already running in
- Prompt telemetry to Cline's tracing backend is now limited to Cline and Cline Pass requests. Turns run against your own provider keys are not traced
- Cline Pass now appears in the composer's provider picker alongside Cline. One Cline sign-in configures both — Cline Pass stores its credentials under the Cline account rather than having its own — but the picker only listed providers with their own saved settings entry, and onboarding writes just the Cline one. A new Cline Pass user therefore saw a single row
- The composer's provider picker has a **Set up another provider** row at the bottom that opens Settings → Models. The picker only lists what you have already configured, so there was no way to reach the rest of the catalog from the composer
- Dropped the "Configured" checkmark from the composer's provider picker (added in 0.0.25). Every row in that picker has a saved settings entry, so for anyone who set their providers up in the app every row carried the same green check and it read as decoration. Settings is still where provider readiness is shown, and it distinguishes a real credential from an entry a legacy migration left behind
## 0.0.25
- ChatGPT Subscription (Codex) now lists only the models your plan can actually use. Two separate paths filled the picker from the shared OpenAI catalog, so GPT-4o, GPT-4.1, and `chatgpt-image-latest` showed up alongside the Codex models, and the runtime lost the Codex context caps. The model rules also match what the backend now accepts: `gpt-5.4` and `gpt-5.4-mini` were retired for ChatGPT accounts on 2026-08-31 and are gone, the default moves to `gpt-5.6-terra`, and every Codex model is capped at the real 400K/272K/128K backend budget instead of inheriting the API's 1.05M limits
- Windows updates no longer fail with "Error opening file for writing". The compiled sidecar re-executes itself as the detached Cline Hub daemon, which outlives the app by design, and Tauri's NSIS installer only kills the main binary — so the daemon still held `code-sidecar.exe` and the install stopped until you killed the process by hand. The installer now stops it first, matched on the full path so updating one channel does not take down a side-by-side Cline Beta's sessions
- Your prompt is no longer lost when a send fails before the turn starts — switching to Codex and having the OAuth refresh throw, for instance. The runtime never took the prompt, so post-send hydration wiped the optimistic bubble and you had to retype it. The text and attachments now come back to the composer, merged with anything you attached while the send was pending, and left alone if you have already started typing something else
- Providers that authenticate through a local CLI — Claude Code, Codex CLI — can now start sessions without an API key. They showed as Configured in Settings via their local-auth capability, but session start still refused them with "Missing API key"
- OpenCode is now treated as a local CLI provider rather than an OAuth one, so it shows the local CLI notice instead of a browser sign-in button that could not do anything. It authenticates from the credentials the opencode CLI itself stores
- Session import from Claude Code, Codex, and opencode has its own page in Settings instead of a row buried in General
- The composer's provider picker now marks which providers you have already configured
- The model picker distinguishes models that share a name, and Cline Pass subscription models are listed separately from the free fallback tier
- Published DMGs use the intended window layout and background again. Tauri skips the Finder AppleScript that applies them whenever `CI` is set, which GitHub Actions always sets, so every DMG since the artwork landed shipped with a stock Finder window even though the artwork was generated and validated
- Cline's recommended, free, and subscribed model lists now ship with the app, so they are correct at first launch instead of waiting on a live catalog fetch
- Refreshed the model catalog. Adds NaN (nan.builders) and changes the resolved default model for 36 providers — including Bedrock, Vertex, OpenRouter, Kilo, GitHub Copilot, Gemini, Cerebras, Fireworks, Requesty, and Vercel AI Gateway. Several move off Claude Fable 5.1 to GPT-6 Astra, Vertex goes to Gemini 3.8 Flash, and OpenRouter/Kilo to Inception Mercury 2.5. If you use one of those without pinning a model, expect a different default
## 0.0.24
- Fixed the live chat stream doubling text and dropping messages mid-turn. The sidecar has two Hub sockets that both receive a session's events — ClineCore's own client and the observer client — and a session that streams without a local send first (a run already in flight when you open the task, a resumed run, a scheduled run) had every delta rendered twice. The observer's copy is now skipped whenever ClineCore is subscribed to the session, asked directly rather than inferred from a timer, so long commands, slow first tokens, and unanswered tool approvals cannot let a duplicate slip through ahead of the core copy. Separately, when the sidecar was replaced under a live webview (crash-respawn, Hub drain-and-replace, stale-sidecar swap) its stream counter restarted at 1 and the webview silently discarded everything until the new process counted past the old run — this dropped your own message bubbles and tool rows, not just assistant text, which is why rows appeared to vanish mid-turn and come back afterwards
- Fixed a queued prompt's own message vanishing from the chat. When you queue a prompt behind a running turn, the runtime drains the queue just before it answers the previous send, so the previous turn's completion path replaced the whole transcript from a canonical read that predated your queued message — erasing your bubble and leaving the reply streaming in under no user message. That path now defers to the newer turn instead of treating the transcript as its own. Two symptoms rode on the same bug: the composer no longer drops out of its busy state while the queued reply is still pending, and a finished reasoning row now reads "Thought for Ns" instead of a stuck "Thinking" — live rows are stamped on the webview's clock, so a sidecar whose clock trails it (a remote Hub, the browser-dev setup) no longer produces a negative duration that gets dropped
- Cline no longer stops silently mid-task when a model gets stuck repeating itself. The loop detector stops a run after 5 identical tool calls and the mistake tracker after 6 consecutive failures, but the desktop never registered a decision callback, so the run just ended and the composer went idle with no message. You are now asked how to continue — "Try a different approach" or "Stop this run" — and the guidance is steered into the running turn so the model knows why it was paused instead of repeating the same call
- The `editor` tool's error message now names the file, says whether `old_text` was null or omitted, and states how to recover. Models that fill optional parameters with null (seen with kimi-k3) hit a terse "old_text is required" and re-sent the identical call until the loop detector stopped the run
- Fixed your Cline Pass model selection being replaced when you start a new chat. Catalogs are discovery data, not validation — the bundled catalog can omit live Cline Pass models and refreshes can return partial lists, so a model missing from the catalog was treated as invalid and silently swapped for a default
- Cline Desktop now has a custom title bar on Windows, with caption controls that follow the compact title-bar height in narrow windows and stay above overlays. The Windows taskbar icon was also updated
- Token counts and costs now fill in for every session you can see. The sessions view only ever hydrated the four most recent rows, so every other row showed "-" and paging never asked for more; the visible page is now hydrated on demand, with reads capped and re-run when a session's status changes underneath them
- Sessions imported from Claude Code, Codex, and opencode now say so in the chat, and their foreign history is summarized on the first resumed turn. Imported transcripts keep the source tool's own tool names and schemas, which a model continuing them may try to call — the summary runs once, the original transcript stays intact, and the "Thinking..." indicator reads "Summarizing the imported <tool> history..." while it happens
- Fixed session history rendering empty when one session had many subagent or team-task children. Child rows always sort after the root that spawned them, so a single busy session could hide itself and every older session from the sidebar with no way to load more
- Checkpoints no longer re-hash every untracked file before each message. Checkpoint creation rebuilt a throwaway git index each turn, so multi-GB untracked data blocked every message for seconds to minutes (~90s in one report on a cloud-synced Windows workspace). One snapshot index is now kept per session, so from the second turn the cost is roughly git process overhead. Snapshot contents are byte-identical to before
- Commands that background a child process (`cmd &`, `nohup`, and the same from Git Bash) no longer hang until the timeout. The inherited stdio pipes stay open after the shell exits, so the completion event never arrived even though the command was done; these now settle with the real exit code and a note that background output is no longer captured
- Typing an `@` mention from your home directory no longer indexes your entire home folder. That could take memory into the gigabytes and get the process killed; the home directory and filesystem root are now skipped entirely
- Web search is now enabled by default outside YOLO mode, and tool settings fail closed if they cannot be loaded
- Claude Code no longer asks for an API key it never reads. It authenticates from the local `claude` CLI's own credential store, but was reported as an API-key provider, so a keyless entry was refused and the workaround was to save a dummy key
- Pasted credentials with invisible characters no longer persist corrupted. A BOM or zero-width character carried in from a copy-paste produced 401s indistinguishable from a wrong key; credential fields are now stripped of control and format characters on save
- Starting a new task no longer flickers through the idle state. The Hub publishes the new session's record as "idle" while the start request is still in flight, so the composer placeholder and the request indicator switched to idle and back for a frame on every new task. A transient idle arriving during a submission is now held back; a real failure or abort still applies immediately
- The model picker keeps section headers visible while you search. Cline Pass lists the same model in both the Subscribed and Free tiers, so flattening the sections during search produced two identical-looking rows
- `apply_patch` "Add File" now refuses to overwrite an existing file instead of silently replacing it
- Fixed session import paths resolving incorrectly on Windows
- The desktop backend now starts off the command path, so startup no longer blocks the UI
- The SDK can now connect to authenticated remote Hubs
## 0.0.23
- Agent Plugins are now discovered and run by the shared Hub. Packages under `~/.agents/plugins` are validated from their `plugin.json`, their valid Agent Skills become available to the agent, and their stdio / Streamable HTTP / SSE MCP servers start automatically. Settings → Customize lists Agent Plugins separately from Cline Plugins, with each plugin's description, badge, and contributed tools, and enable/disable is Hub-managed per plugin. Workspace `.agents/plugins` directories are intentionally ignored
- The "Cline Hub was updated" dialog no longer appears on every launch and reconnect. The app no longer prompts about a Hub running the same core version it does — a desktop and CLI release cut from different commits bundle the same core but never share a build fingerprint, so anyone with both installed got a dialog whose "Update and restart" looped on "no app update available". The build-mismatch dialog now also waits until an app update is actually staged, and "Later" sticks across session switches, reloads, and relaunches instead of resurfacing every time. A Hub the app genuinely cannot talk to still warns every time
- Signing in now shows the device confirmation code in the app while you wait on the browser, so you can match it against the code the browser asks you to confirm — in onboarding, Account settings, and the provider list
- Voice input failures caused by provider setup — missing credentials, transcription config — now take you straight to voice settings instead of a toast you cannot act on. Genuine microphone permission failures still toast, with a clearer message
- Fixed the scheduled-task report vanishing when a finished run's step collapsed
- Fixed one wedged MCP server blocking the rest from shutting down, leaking their processes
- Beta: Composio connectors now register tools directly in the packaged desktop runtime for eligible internal accounts, with safer OAuth revocation and more resilient connect, disconnect, and reconciliation behavior.
- Web search is enabled by default for new desktop sessions.
- Includes all stable desktop improvements through 0.0.22, including the two-pane Marketplace explorer, reliable cancellation of child agents and teammates, full-composer attachment drops, live Cline model catalog refreshes, and clearer provider authentication errors.
## 0.0.22
@@ -117,6 +37,13 @@
- Fixed Langfuse tracing never initializing in release builds — the minified bundle broke tracer detection, so telemetry worked in dev and silently did nothing in the shipped app. Also updated for AI SDK 7's telemetry API
- Refreshed the model catalog. Adds TokenGo and Volcengine Ark, and updates model lists, pricing, and the resolved default model for ~36 providers (including Hugging Face, Mistral, OpenRouter, Together, NanoGPT, Requesty, Baseten, Cloudflare Workers AI, and DigitalOcean) — if you use one of those without pinning a model, you will get a different default
## 0.0.21-beta.2
- Beta: hand off local sessions to Cline Cloud and continue working from cloud workspaces, with recovery for interrupted transfers and preservation of the prompt, attachments, and session state.
- Beta: choose between local, SSH remote, and Cloud environments from the desktop app, with the experimental realtime voice and avatar overlay experiences included.
- Beta: the GitHub onboarding step is available behind the `code-onboarding-github` feature flag and remains disabled by default.
- Includes all stable desktop improvements through 0.0.20, including the Windows release, full-history session search, scheduled-task fixes, inline tool-result images, and the latest provider and Marketplace updates.
## 0.0.20
- Customize now separates Cline Plugins from Agent Plugins discovered by the Hub. Agent Plugin switches use Hub-managed enablement, contributed skills appear in the Skills inventory, and connected desktop views refresh when Hub settings change
@@ -190,6 +117,13 @@
- Refreshed the model catalog, which updates model lists and pricing across providers and changes the resolved default model for several of them (DeepSeek, Crof, CrossModel, Eden AI, Kilo, and NanoGPT)
- The app now honors server-side feature flags, refreshing them when your account changes
## 0.0.16-beta.1
- Beta: your typed prompt is no longer lost when a cloud handoff starts — the live draft carries into the handoff and is restored if it fails.
- Beta: fixed a closed model popover blocking clicks in the composer.
- Beta: cleaned up visual regressions in provider settings, notifications, and the avatar overlay from the previous sync.
- Includes the 0.0.16-track main updates: the redesigned first-run onboarding with an interactive welcome graphic, centralized tool availability, hook fixes (PostToolUse output and context changes now reach the model), and the fix for checkpoint restore staying locked after queued turns.
## 0.0.15
- The app is now called Cline, renamed from Cline Code. Your settings, sessions, and credentials carry over untouched — only the name and icon change
@@ -212,6 +146,13 @@
- Usage now displays the billed gateway cost
- Refreshed the model catalog, which adds AMD, Arcee, Echo, Jalapeno, Kosmik, LLM Gateway, RunInfra, and SCNet as providers and updates model lists, pricing, and per-provider default models across the board
## 0.0.15-beta.1
- Beta: hand off a local session to Cline Cloud with `/handoff` — the conversation, attached images, and an optional follow-up command move to a cloud workspace that keeps working after you close the app. A preflight confirms the repository, branch, and commit are pushed; the composer shows handoff progress and finishes with a receipt linking to the cloud session.
- Beta: Cloud now lives in the existing Local / Remote environment menu. It is selectable when the Cloud sessions feature flag is on and shows as "Coming soon" otherwise. The separate Local / Cloud toggle is gone; repository and branch controls are unchanged.
- If a handoff is interrupted — the app restarts, the network drops, or the branch moves mid-transfer — reopening the session recovers or cleanly retries it, and your typed draft and attachments are restored.
- Includes the 0.0.14 stable release and everything on main since: the unified Plugins hub with a Marketplace page, recommended and free model tiers in the model picker, scheduled tasks for agents, and the app rename to "Cline" (this beta is now "Cline Beta").
## 0.0.14
- The app now posts native macOS notifications when a task finishes or needs your input, so you can leave Cline working in the background. Configure them under Settings → Notifications.
@@ -236,6 +177,15 @@
- Fixed misaligned columns in the Usage table, and added a See More link to the full usage dashboard.
- Fixed routine dialog dropdowns not responding to mouse clicks.
## 0.0.14-beta.1
- First beta release. Cline Code Beta installs side by side with the stable app so you can compare the two, and updates automatically from its own beta channel — stable installs are unaffected.
- Cloud sessions (preview): run sessions in Cline's cloud straight from the desktop app. Connect GitHub during onboarding, pick a repository and branch, and hand sessions off between devices — transcripts, approvals, and queued prompts stay in sync, and you can rename cloud sessions and switch models mid-session. Turn it on with the Cloud sessions toggle in Settings.
- Avatar overlay (preview): a floating desktop companion that reacts to what your sessions are doing.
- Onboarding now includes a GitHub integration step.
- Early proof of concept for running sessions in SSH remote environments.
- Includes everything from the upcoming stable release: microphone voice input in the composer, model-driven image generation, redesigned question prompts, animated reasoning and tool disclosures, and session list polish.
## 0.0.13
- Added an app font size setting. A slider in Settings scales the interface, and your size is applied before the window paints, so launching no longer flashes at the old size first.
+64 -47
View File
@@ -10,59 +10,13 @@ From `apps/examples/desktop-app/`:
- `bun run dev:web` - Next.js UI only (approval-gated tools require `dev:headless` or the native app)
- `bun run dev:sidecar` - sidecar backend only (approval-gated tools require `dev:headless` or the native app)
- `bun run dev` - Tauri desktop dev
- `bun run build:web` - build production web assets only (includes the shared UI build)
- `bun run build` - build web assets and the sidecar binary
- `bun run build` - build web assets
- `bun run build:sidecar` - build the Bun sidecar bundle
- `bun run build:sidecar:bin` - compile the Bun sidecar into a local binary
- `bun run build:binary` - build desktop binary
- `bun run package:desktop` - package the current OS desktop app into `dist/desktop/`
- `bun run typecheck` - TypeScript check
### Checking webview changes
Run `bun run build:web` from this directory when changing webview imports or shared browser APIs. Type checking and Vitest do not check the production browser bundle: a valid TypeScript import can still pull Node-only modules into a client chunk. Use `@cline/shared/browser` for runtime imports in the webview; the bare `@cline/shared` source alias points to the Node entry point.
## Pull Requests
The composer shows the current branch's GitHub pull request, merge status,
changed-line totals, and CI checks. Click the PR number to open it in your
browser, or expand CI to inspect individual checks and their logs. Status
refreshes every 30 seconds while visible, when the app regains focus, and
when you click refresh.
This requires GitHub CLI (`gh`) installed and authenticated with `gh auth login`,
and a GitHub.com `origin` remote (HTTPS or SSH). The row is hidden for the
default branch, detached HEAD, and unsupported repositories. If the branch
has no PR, **Create PR** opens GitHub's comparison form; push your commits
before submitting the form. The app does not push commits or submit PRs itself.
Missing or unauthenticated GitHub CLI also hides the row. Availability checks
are shared across workspaces and cached for five minutes, so unavailable CLI
installs do not spawn a failing process on every poll or window focus. After
installing or signing into `gh`, the feature becomes available on the first
refresh after the cache expires (or after restarting the desktop backend).
Initial lookup failures stay hidden. Errors after a successful status load
can be dismissed and remain dismissed through retries until a load succeeds.
### Pull request telemetry
These events use the desktop telemetry service and respect telemetry opt-out:
| Event | Trigger |
| --- | --- |
| `desktop.pull_request.shown` | First visible PR/create row per mounted workspace and branch |
| `desktop.pull_request.open_clicked` | Click the PR link |
| `desktop.pull_request.create_clicked` | Click Create PR (intent only, not PR submission) |
| `desktop.pull_request.checks_expanded` | Open the CI popover |
| `desktop.pull_request.check_clicked` | Click a check's details link |
| `desktop.pull_request.refresh_clicked` | Click manual refresh |
Each event contains only `prState`, `ciState`, and `mergeTone` categories.
The sidecar validates these values and strips extra fields. Repository/branch
names, paths, PR numbers/titles, check names, and URLs are not included.
Automatic polling does not emit additional impressions. Telemetry delivery
does not block interactions, and failures do not interrupt the feature.
## Customizing the macOS Install Window
The drag-to-Applications window is configured by `bundle.macOS.dmg` in
@@ -109,6 +63,59 @@ API keys, `JAVA_HOME`-style tool roots) are not pulled in. Set
`CLINE_SIDECAR_SKIP_SHELL_PATH=1` to disable. Implementation and details:
[`sidecar/shell-path.ts`](./sidecar/shell-path.ts).
## SSH Remote Environments (v0)
Open **Settings → Remote** to add and test an SSH host. Saving or testing a
profile does not activate it. From the welcome chat, open the environment
selector beside the workspace picker and choose the saved host; that selection
starts the SSH connection at the remote user's home directory. Choose **Add
project…** from the normal workspace selector to browse that machine and select
a project, or choose **Local** in the environment selector to disconnect. Recent
and last-used workspaces are remembered separately for each SSH host and for
the local machine.
SSH config aliases are supported. Leave **Port** blank to use the alias's SSH
configuration (including its configured port), or enter a port to override it.
The desktop keeps its webview and native integration local; only the
authenticated Cline Hub protocol is forwarded through SSH. Agent tools,
workspace discovery, Git metadata, and session persistence therefore run on the
SSH host, while approvals and live session events return to the desktop.
The desktop stores host metadata at
`~/.cline/data/settings/remote-environments.json` with mode `0600`. It stores an
identity-file path, never private-key contents. On first connect it uploads a
content-addressed, branch-matched, self-contained Hub helper under
`~/.cline/code/remote/`, binds the Hub to remote loopback, and forwards it to a
random local loopback port. Linux x64 and arm64 helpers are bundled by
`bun run build:sidecar:bin`; 32-bit Raspberry Pi operating systems are not
supported in v0. The current helper is roughly 110 MB because it includes its
own runtime. It is copied once per matching desktop build and cached, with no
`apt`, `npm`, root access,
global CLI install, or public Hub port. Disconnecting stops the desktop-owned
remote Hub but leaves the helper cached for a faster reconnect. The helper
imports the remote login-shell `PATH`, so user-installed Git, GitHub CLI, and
MCP executables remain visible.
The desktop uses its own discovery record, so an existing Cline CLI/Hub on the
same account is neither replaced nor stopped. Both Hub processes can coexist
while the desktop is connected; this isolation is intentional for the proof of
concept so a branch-matched desktop helper cannot disrupt another Cline build.
v0 intentionally leaves file attachments and opening a remote file in a local
editor disabled. Text, images, file mentions/search, Git branch operations,
session history, and remote agent tools are supported. The current desktop
provider access/API token is sent through the authenticated tunnel for the
session; reusable OAuth refresh credentials are not copied into remote provider
settings.
For a real SSH acceptance run, `scripts/verify-ssh-poc.ts` accepts
`CLINE_SSH_TEST_HOST`, `CLINE_SSH_TEST_USER`, `CLINE_SSH_TEST_KEY`,
`CLINE_SSH_TEST_WORKSPACE`, and `CLINE_SSH_TEST_HELPER`. It starts a remote
connection at the SSH user's home, starts an agent session in the test
workspace with the selected desktop provider, asks the agent to read
`REMOTE_MARKER.txt`, then verifies the session appears in remote history and
that its messages can be read back.
## Web Visual System
The framework-neutral color, typography, radius, and navigation contract lives
@@ -220,6 +227,16 @@ Desktop transport envelope:
## Data + Storage
- Session artifacts are written under `~/.cline/data/sessions/<sessionId>/` (or `CLINE_SESSION_DATA_DIR`).
- Desktop avatar packages live under `~/.cline/avatars/<avatar-name>/`. Each package
contains a v2 `spritesheet.webp` (or PNG) and either an `avatar.json` or
`pet.json` manifest with
`id`, `displayName`, `description`, `spriteVersionNumber: 2`, and
`spritesheetPath`. If both manifests exist, `avatar.json` takes precedence.
The bundled Cline Bot avatar is selected and enabled by default, with Mom also
available as a bundled option.
Visibility and the selected installed avatar are configured independently under
**Settings → General → Desktop avatar**; both values are stored in
`~/.cline/avatars/selected.json`.
- Canonical replay/export artifact: `<sessionId>.messages.json`.
- `<sessionId>.messages.json` is expected to contain ordered messages plus assistant `modelInfo` and `metrics` (including cache token fields when provided by the model runtime).
- `<sessionId>.hooks.jsonl` is observability/debug telemetry and should not be required for normal history replay/export flows.
+10 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@cline/code",
"version": "0.0.28",
"version": "0.0.23-beta.1",
"private": true,
"scripts": {
"build:ui": "bun -F @cline/ui build",
@@ -10,8 +10,6 @@
"predev:headless": "bun run build:ui",
"dev:headless": "bun run scripts/dev-headless.ts",
"dev": "tauri dev --config src-tauri/tauri.dev.conf.json",
"prebuild:web": "bun run build:ui",
"build:web": "next build webview",
"prebuild": "bun run build:ui",
"build": "bun run bun.mts",
"build:sidecar": "mkdir -p dist/sidecar && bun build ./sidecar/index.ts --outfile ./dist/sidecar/index.js --target bun",
@@ -19,6 +17,7 @@
"build:binary": "tauri build",
"dmg:background": "bun run scripts/dmg-background.ts",
"test:dmg-background": "bun test scripts/dmg-background.test.ts",
"test:bun-cross-compile-runtime": "bun test scripts/bun-cross-compile-runtime.test.ts",
"package": "bun run package:desktop",
"package:desktop": "bun run scripts/package-desktop.ts",
"package:desktop:mac": "bun run scripts/package-desktop.ts --platform mac",
@@ -29,17 +28,20 @@
"typecheck": "tsc -p tsconfig.dev.json --noEmit",
"pretest:chat-ui": "bun run build:ui",
"test:chat-ui": "vitest run webview/components/views/chat/chat-messages.test.tsx webview/components/views/chat/messages --config vitest.config.ts",
"pretest:settings-ui": "bun run build:ui",
"test:settings-ui": "vitest run webview/components/views/settings webview/components/ui/switch-integration.test.tsx --config vitest.config.ts",
"test:sidecar": "vitest run sidecar scripts/telemetry-define-args.test.ts --config vitest.config.ts",
"clean": "rm -rf webview/.next webview/out node_modules dist && (cd src-tauri && rm -rf target node_modules dist)"
},
"dependencies": {
"@ai-sdk/gateway": "4.0.31",
"@ai-sdk/google": "4.0.44",
"@ai-sdk/openai": "4.0.41",
"@ai-sdk/react": "4.0.44",
"@base-ui/react": "^1.2.0",
"@cline/core": "workspace:*",
"@cline/llms": "workspace:*",
"@composio/core": "^0.18.0",
"@cline/shared": "workspace:*",
"@cline/ui": "workspace:*",
"ai": "^7.0.58",
"@fontsource-variable/geist-mono": "^5.2.8",
"@fontsource-variable/inter": "^5.2.8",
"@hookform/resolvers": "^3.9.1",
@@ -65,6 +67,7 @@
"@radix-ui/react-separator": "1.1.8",
"@radix-ui/react-slider": "1.3.6",
"@radix-ui/react-slot": "1.2.4",
"@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-toggle": "1.1.10",
@@ -98,7 +101,7 @@
"streamdown": "^2.5.0",
"tailwind-merge": "^3.3.1",
"vaul": "^1.1.2",
"zod": "^3.24.1"
"zod": "^3.25.76"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0",
@@ -1,4 +1,5 @@
import { $ } from "bun";
import { prepareWindowsCrossCompileRuntime } from "./bun-cross-compile-runtime";
import { telemetryDefineArgs } from "./telemetry-define-args";
const resolveTargetTriple = async (): Promise<string> => {
@@ -38,22 +39,55 @@ const sidecarOutfile = (targetTriple: string): string => {
return `./src-tauri/bin/code-sidecar-${targetTriple}${extension}`;
};
const buildSidecar = async (targetTriple: string): Promise<string> => {
const outfile = sidecarOutfile(targetTriple);
const buildSidecar = async (
targetTriple: string,
outfile = sidecarOutfile(targetTriple),
entrypoint = "./sidecar/index.ts",
minify = false,
): Promise<string> => {
const bunTarget = resolveBunCompileTarget(targetTriple);
// Telemetry config must be inlined into the compiled binary: a packaged
// app launched from Finder/the Dock has no OTEL_* env at runtime, so
// without this the sidecar silently ships with telemetry disabled.
// Verify with `<binary> --telemetry-selfcheck` after building.
const defines = telemetryDefineArgs();
const optimizationArgs = minify ? ["--minify"] : [];
// A compiled Bun executable otherwise reads .env and bunfig.toml from its
// launch directory before our entrypoint runs. Remote helpers are launched
// from an SSH user's home directory, so that behavior can both make the
// helper fail on an unrelated dotenv file and leak workspace credentials
// into the Hub process. Packaged binaries must depend only on their explicit
// process environment and compiled configuration.
const runtimeIsolationArgs = [
"--no-compile-autoload-dotenv",
"--no-compile-autoload-bunfig",
];
if (bunTarget) {
await $`bun build ./sidecar/index.ts --compile --target=${bunTarget} ${defines} --outfile ${outfile}`;
await prepareWindowsCrossCompileRuntime(bunTarget);
await $`bun build ${entrypoint} --compile --target=${bunTarget} ${runtimeIsolationArgs} ${optimizationArgs} ${defines} --outfile ${outfile}`;
} else {
await $`bun build ./sidecar/index.ts --compile ${defines} --outfile ${outfile}`;
await $`bun build ${entrypoint} --compile ${runtimeIsolationArgs} ${optimizationArgs} ${defines} --outfile ${outfile}`;
}
return outfile;
};
// SSH environments run the same Hub build as the desktop in a dedicated
// bootstrap/daemon binary. It intentionally excludes the desktop HTTP server,
// command router, and UI backend. Linux x64 and arm64 cover common SSH hosts.
const buildRemoteHelpers = async (): Promise<void> => {
for (const targetTriple of [
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
]) {
await buildSidecar(
targetTriple,
`./src-tauri/bin/remote-helpers/code-sidecar-${targetTriple}`,
"./sidecar/remote-helper.ts",
true,
);
}
};
// Tauri's universal-apple-darwin pseudo-target lipos the Rust binary itself
// but expects sidecars (externalBin) to already be fat binaries named
// `<name>-universal-apple-darwin`, so build both slices and merge them here.
@@ -68,12 +102,13 @@ const buildUniversalMacSidecar = async (): Promise<void> => {
const main = async () => {
const targetTriple = await resolveTargetTriple();
await $`mkdir -p src-tauri/bin`;
await $`mkdir -p src-tauri/bin src-tauri/bin/remote-helpers`;
if (targetTriple === "universal-apple-darwin") {
await buildUniversalMacSidecar();
return;
} else {
await buildSidecar(targetTriple);
}
await buildSidecar(targetTriple);
await buildRemoteHelpers();
};
main().catch((error: unknown) => {
@@ -0,0 +1,66 @@
import { describe, expect, test } from "bun:test";
import {
isExpectedElfExecutable,
resolveWindowsCrossCompileRuntime,
} from "./bun-cross-compile-runtime";
describe("resolveWindowsCrossCompileRuntime", () => {
test("maps Bun's x64 target to its pinned release asset and cache name", () => {
expect(
resolveWindowsCrossCompileRuntime("bun-linux-x64", "1.3.13"),
).toMatchObject({
archiveName: "bun-linux-x64",
cacheFilename: "bun-linux-x64-v1.3.13",
downloadUrl:
"https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-linux-x64.zip",
expectedMachine: 62,
});
});
test("maps Bun's arm64 target to its aarch64 asset and cache name", () => {
expect(
resolveWindowsCrossCompileRuntime("bun-linux-arm64", "1.3.13"),
).toMatchObject({
archiveName: "bun-linux-aarch64",
cacheFilename: "bun-linux-aarch64-v1.3.13",
expectedMachine: 183,
});
});
test("does not intercept native or non-Linux targets", () => {
expect(
resolveWindowsCrossCompileRuntime("bun-windows-x64", "1.3.13"),
).toBeUndefined();
expect(
resolveWindowsCrossCompileRuntime("bun-darwin-arm64", "1.3.13"),
).toBeUndefined();
});
test("fails closed when Bun changes without updated checksums", () => {
expect(() =>
resolveWindowsCrossCompileRuntime("bun-linux-x64", "1.3.14"),
).toThrow("requires Bun 1.3.13");
});
});
describe("isExpectedElfExecutable", () => {
const elfHeader = (machine: number): Uint8Array => {
const header = new Uint8Array(20);
header.set([0x7f, 0x45, 0x4c, 0x46]);
header[5] = 1;
header[18] = machine & 0xff;
header[19] = (machine >> 8) & 0xff;
return header;
};
test("accepts the expected little-endian ELF architecture", () => {
expect(isExpectedElfExecutable(elfHeader(62), 62)).toBe(true);
expect(isExpectedElfExecutable(elfHeader(183), 183)).toBe(true);
});
test("rejects the wrong architecture and malformed files", () => {
expect(isExpectedElfExecutable(elfHeader(62), 183)).toBe(false);
expect(isExpectedElfExecutable(new Uint8Array(20), 62)).toBe(false);
expect(isExpectedElfExecutable(new Uint8Array(10), 62)).toBe(false);
});
});
@@ -0,0 +1,192 @@
import { createHash } from "node:crypto";
import {
copyFile,
mkdir,
mkdtemp,
open,
rename,
rm,
stat,
} from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import path from "node:path";
const PINNED_BUN_VERSION = "1.3.13";
type RuntimeSpec = {
archiveName: string;
cacheName: string;
expectedMachine: number;
sha256: string;
};
const WINDOWS_LINUX_RUNTIME_SPECS: Record<string, RuntimeSpec> = {
"bun-linux-x64": {
archiveName: "bun-linux-x64",
cacheName: "bun-linux-x64",
expectedMachine: 62,
sha256: "79c0771fa8b92c33aae41e15a0e0d307ea99d0e2f00317c71c6c53237a78e25a",
},
"bun-linux-arm64": {
archiveName: "bun-linux-aarch64",
cacheName: "bun-linux-aarch64",
expectedMachine: 183,
sha256: "70bae41b3908b0a120e1e58c5c8af30e74afae3b8d11b0d3fdd8e787ddfb4b22",
},
};
export type WindowsCrossCompileRuntime = RuntimeSpec & {
cacheFilename: string;
downloadUrl: string;
};
export const resolveWindowsCrossCompileRuntime = (
bunTarget: string,
bunVersion: string,
): WindowsCrossCompileRuntime | undefined => {
const spec = WINDOWS_LINUX_RUNTIME_SPECS[bunTarget];
if (!spec) return undefined;
if (bunVersion !== PINNED_BUN_VERSION) {
throw new Error(
`Windows Linux cross-compilation requires Bun ${PINNED_BUN_VERSION}; received ${bunVersion}`,
);
}
return {
...spec,
cacheFilename: `${spec.cacheName}-v${bunVersion}`,
downloadUrl: `https://github.com/oven-sh/bun/releases/download/bun-v${bunVersion}/${spec.archiveName}.zip`,
};
};
export const isExpectedElfExecutable = (
header: Uint8Array,
expectedMachine: number,
): boolean =>
header.length >= 20 &&
header[0] === 0x7f &&
header[1] === 0x45 &&
header[2] === 0x4c &&
header[3] === 0x46 &&
header[5] === 1 &&
header[18] === (expectedMachine & 0xff) &&
header[19] === ((expectedMachine >> 8) & 0xff);
const hasExpectedRuntime = async (
filePath: string,
expectedMachine: number,
): Promise<boolean> => {
try {
const fileStat = await stat(filePath);
if (!fileStat.isFile() || fileStat.size < 1_000_000) return false;
const file = await open(filePath, "r");
try {
const header = new Uint8Array(20);
const { bytesRead } = await file.read(header, 0, header.length, 0);
return (
bytesRead === header.length &&
isExpectedElfExecutable(header, expectedMachine)
);
} finally {
await file.close();
}
} catch {
return false;
}
};
const powershellLiteral = (value: string): string =>
`'${value.replaceAll("'", "''")}'`;
/**
* Bun cannot currently unpack Linux cross-compile runtimes on Windows. Put the
* official pinned runtime in Bun's normal cache so `bun build --compile` can
* use it without taking the broken extractor path.
*/
export const prepareWindowsCrossCompileRuntime = async (
bunTarget: string,
): Promise<void> => {
if (process.platform !== "win32") return;
const spec = resolveWindowsCrossCompileRuntime(bunTarget, Bun.version);
if (!spec) return;
const bunInstall =
process.env.BUN_INSTALL?.trim() || path.join(homedir(), ".bun");
const cacheDir = path.join(bunInstall, "install", "cache");
const cachePath = path.join(cacheDir, spec.cacheFilename);
if (await hasExpectedRuntime(cachePath, spec.expectedMachine)) return;
const stagingDir = await mkdtemp(
path.join(tmpdir(), `cline-${spec.archiveName}-`),
);
try {
const archivePath = path.join(stagingDir, `${spec.archiveName}.zip`);
const extractDir = path.join(stagingDir, "extracted");
const response = await fetch(spec.downloadUrl);
if (!response.ok) {
throw new Error(
`Failed to download ${spec.archiveName}: HTTP ${response.status}`,
);
}
const archive = new Uint8Array(await response.arrayBuffer());
const actualSha256 = createHash("sha256").update(archive).digest("hex");
if (actualSha256 !== spec.sha256) {
throw new Error(
`${spec.archiveName} checksum mismatch: expected ${spec.sha256}, received ${actualSha256}`,
);
}
await Bun.write(archivePath, archive);
await mkdir(extractDir, { recursive: true });
const expandCommand = `Expand-Archive -LiteralPath ${powershellLiteral(archivePath)} -DestinationPath ${powershellLiteral(extractDir)} -Force`;
const expand = Bun.spawn(
[
"powershell.exe",
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-Command",
expandCommand,
],
{ stdout: "inherit", stderr: "inherit" },
);
const expandExitCode = await expand.exited;
if (expandExitCode !== 0) {
throw new Error(
`Failed to extract ${spec.archiveName} (exit ${expandExitCode})`,
);
}
const extractedRuntime = path.join(extractDir, spec.archiveName, "bun");
if (!(await hasExpectedRuntime(extractedRuntime, spec.expectedMachine))) {
throw new Error(
`${spec.archiveName} did not contain the expected Linux executable`,
);
}
await mkdir(cacheDir, { recursive: true });
const stagedCachePath = `${cachePath}.${process.pid}.tmp`;
await copyFile(extractedRuntime, stagedCachePath);
await rm(cachePath, { force: true, recursive: true });
await rename(stagedCachePath, cachePath);
console.log(`Prepared Bun cross-compile runtime: ${spec.cacheFilename}`);
} finally {
await rm(stagingDir, { force: true, recursive: true });
}
};
const main = async (): Promise<void> => {
const targets = process.argv.slice(2);
if (targets.length === 0) {
throw new Error("Pass at least one Bun compile target to prepare");
}
for (const target of targets) {
await prepareWindowsCrossCompileRuntime(target);
}
};
if (import.meta.main) {
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
}
@@ -0,0 +1,176 @@
import { mkdtemp, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
ClineCore,
ProviderSettingsManager,
RuntimeOAuthTokenManager,
resolveProviderApiKeyFromSettings,
SessionSource,
toProviderConfig,
} from "@cline/core";
import { RemoteEnvironmentService } from "../sidecar/remote-environments";
const required = (name: string): string => {
const value = process.env[name]?.trim();
if (!value) throw new Error(`${name} is required`);
return value;
};
async function main(): Promise<void> {
const temporaryDirectory = await mkdtemp(join(tmpdir(), "cline-ssh-proof-"));
const service = new RemoteEnvironmentService({
profilesPath: join(temporaryDirectory, "remote-environments.json"),
helperBinaryPath: required("CLINE_SSH_TEST_HELPER"),
knownHostsPath: join(temporaryDirectory, "known_hosts"),
commandTimeoutMs: 60_000,
uploadTimeoutMs: 5 * 60_000,
});
let core: ClineCore | undefined;
try {
const helperPath = required("CLINE_SSH_TEST_HELPER");
const workspaceRoot = required("CLINE_SSH_TEST_WORKSPACE");
const profile = await service.upsert({
name: "SSH proof host",
host: required("CLINE_SSH_TEST_HOST"),
user: process.env.CLINE_SSH_TEST_USER?.trim() || undefined,
identityFile: required("CLINE_SSH_TEST_KEY"),
});
const connection = await service.connect(profile.id);
const marker = await service.run(profile.id, {
command: "sed",
args: ["-n", "1p", "REMOTE_MARKER.txt"],
cwd: workspaceRoot,
});
const providerSettings = new ProviderSettingsManager();
const stored = providerSettings.read();
const providerId = stored.lastUsedProvider;
if (!providerId)
throw new Error("No configured desktop provider is available");
const settings = providerSettings.getProviderSettings(providerId);
if (!settings)
throw new Error(`No settings found for provider ${providerId}`);
const modelId = settings.model || "meta/muse-spark-1.2";
const providerConfig = {
...toProviderConfig(
{ ...settings, model: modelId },
{ includeKnownModels: false },
),
};
delete providerConfig.refreshToken;
const oauth = await new RuntimeOAuthTokenManager({
providerSettingsManager: providerSettings,
}).resolveProviderApiKey({ providerId });
const apiKey =
oauth?.apiKey ||
resolveProviderApiKeyFromSettings(providerSettings, providerId);
if (!apiKey)
throw new Error(`No credential found for provider ${providerId}`);
core = await ClineCore.create({
clientName: "cline-code",
backendMode: "remote",
remote: {
endpoint: connection.endpoint,
authToken: connection.authToken,
workspaceRoot: connection.workspaceRoot,
cwd: connection.workspaceRoot,
clientType: "code-sidecar-ssh",
},
});
const eventNames: string[] = [];
const unsubscribe = core.subscribe((event) => {
eventNames.push(event.type);
});
const started = await core.start({
config: {
providerId,
modelId,
apiKey,
providerConfig,
workspaceRoot,
cwd: workspaceRoot,
systemPrompt: "",
mode: "act",
enableTools: true,
enableSpawnAgent: false,
enableAgentTeams: false,
},
source: SessionSource.DESKTOP,
interactive: true,
toolPolicies: { "*": { autoApprove: true } },
});
const result = await core.send({
sessionId: started.sessionId,
prompt:
"Read REMOTE_MARKER.txt from this workspace with the file-reading tool, then reply with its exact contents. Do not change any files.",
});
const sessions = await core.list(20, { hydrate: false });
const messages = await core.readMessages(started.sessionId);
unsubscribe();
await core.dispose("desktop_ssh_proof_reconnect");
core = undefined;
await service.disconnect(profile.id);
const reconnected = await service.connect(profile.id);
core = await ClineCore.create({
clientName: "cline-code",
backendMode: "remote",
remote: {
endpoint: reconnected.endpoint,
authToken: reconnected.authToken,
workspaceRoot: reconnected.workspaceRoot,
cwd: reconnected.workspaceRoot,
clientType: "code-sidecar-ssh",
},
});
const sessionsAfterReconnect = await core.list(20, { hydrate: false });
const messagesAfterReconnect = await core.readMessages(started.sessionId);
const resultText = result?.text ?? "";
const report = {
connected: true,
remote: `${connection.platform}/${connection.arch}`,
connectionRoot: connection.workspaceRoot,
workspaceRoot: started.manifest.workspace_root,
sessionId: started.sessionId,
listContainsSession: sessions.some(
(session) => session.sessionId === started.sessionId,
),
messageCount: messages.length,
reconnected: true,
reconnectListContainsSession: sessionsAfterReconnect.some(
(session) => session.sessionId === started.sessionId,
),
reconnectMessageCount: messagesAfterReconnect.length,
helperBytes: (await stat(helperPath)).size,
sshMarker: marker.stdout.trim(),
agentText: resultText,
agentObservedMarker: resultText.includes("remote workspace proof"),
eventNames: [...new Set(eventNames)],
};
if (
report.sshMarker !== "remote workspace proof" ||
!report.agentObservedMarker ||
!report.listContainsSession ||
!report.reconnectListContainsSession ||
report.messageCount < 2 ||
report.reconnectMessageCount < 2 ||
!report.eventNames.includes("agent_event")
) {
throw new Error(`SSH proof failed: ${JSON.stringify(report)}`);
}
process.stdout.write(`${JSON.stringify(report)}\n`);
} finally {
await core?.dispose("desktop_ssh_proof_complete");
await service.dispose();
await rm(temporaryDirectory, { recursive: true, force: true });
}
}
void main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
@@ -16,9 +16,12 @@ sidecar/
├── index.ts # Entry point: starts HTTP+WS server
├── server.ts # Bun HTTP server + WebSocket handlers
├── context.ts # SidecarContext type and factory
├── client-context.ts # Desktop client/account identity for shared telemetry
├── commands.ts # Command router
├── chat-session.ts # Shared-Hub chat session adapter
├── chat-session.ts # Shared-Hub chat session adapter (local + cloud routing)
├── cloud-sessions.ts # Cloud session REST client + Hub-proxy manager
├── cline-auth.ts # Refresh-aware Cline auth token resolution
├── desktop-settings.ts # Desktop-owned settings (cloud sessions opt-in)
├── feature-flags.ts # Cloud sessions gate (env override + settings toggle)
├── session-data/ # Shared discovery, messages, artifacts, search helpers
├── paths.ts # Path resolution
├── types.ts # Shared types
@@ -82,15 +85,6 @@ The compiled sidecar also recognizes Core's Hub-daemon launch mode. This lets
the desktop start the same detached Hub when no CLI process has started it yet.
Startup discovery and locking ensure concurrent clients converge on one Hub.
Every create, restart, fork, and restore also attaches the serializable Desktop
`ExtensionContext.client` and current `ExtensionContext.user`. Core forwards
that context across the Hub transport and scopes the daemon-owned telemetry
service to the originating surface. This keeps lifecycle events centralized in
Core while reporting Desktop dimensions (`cline_type: "desktop"`, `platform:
"Cline Desktop"`, and the Desktop app version) and the current account and
organization. The shared Hub telemetry singleton is never mutated per session,
so concurrent CLI and Desktop tasks retain their own attribution.
### 2. Tool Approval — Client-Owned Promise Resolution
The shared Hub routes approval requests back to the client that created the
@@ -99,7 +93,7 @@ online:
```typescript
const pendingApprovals = new Map<string, {
resolve: (result: ToolApprovalResult) => void;
resolve: (result: ToolApprovalResult) => void | Promise<void>;
request: ToolApprovalRequest;
}>();
@@ -107,6 +101,10 @@ const pendingApprovals = new Map<string, {
// When frontend responds → resolve promise
```
Cloud sessions route approvals the same way, but the resolver forwards the
response to the sandbox Hub (`approval.respond`), which is why `resolve` may
be async.
### 3. Provider Management — Direct ProviderSettingsManager
```typescript
@@ -144,22 +142,11 @@ The frontend `desktop-client.ts` connects directly to the sidecar WebSocket:
## Command Map
The model picker first uses `list_provider_catalog`, which reads the bundled and
registered models without network access. It then calls `list_provider_models`
for the active provider, both on mount and when the provider changes. All built-in
providers backed by the shared catalog refresh from the live feed (including
OpenCode); concurrent requests share one fetch and reuse its ten-minute cache.
Endpoint-owned lists such as Baseten, Hicap, Poolside, LiteLLM, Ollama, and LM Studio use their existing
discovery endpoints instead. Catalog and public endpoint requests time out after
five seconds, and the initial picker remains usable while a refresh is pending.
The sidecar omits bundled `knownModels` from the discovery config so they cannot
override live metadata; explicitly registered model overrides retain precedence.
Supported commands:
| Command | Implementation |
|---------|---------------|
| `chat_session_command` | shared Hub through `ClineCore` |
| `chat_session_command` | shared Hub through `ClineCore`; cloud sessions route to `CloudSessionManager` |
| `list_provider_catalog` | `ProviderSettingsManager` + `listLocalProviders` |
| `list_provider_models` | `getLocalProviderModels` |
| `save_voice_input_settings` | validates and persists the selected transcription provider/model |
@@ -168,18 +155,30 @@ Supported commands:
| `save_provider_settings` | `saveLocalProviderSettings` |
| `add_provider` | `addLocalProvider` |
| `run_provider_oauth_login` | `loginLocalProvider` |
| `list_chat_sessions` | `SqliteSessionStore` + file discovery |
| `list_discovered_sessions` | Merged discovery |
| `read_session_messages` | Session data readers |
| `list_chat_sessions` | `SqliteSessionStore` + file discovery, merged with cloud sessions (2s budget) |
| `list_discovered_sessions` | Merged discovery (local + cloud) |
| `read_session_messages` | Session data readers; cloud sessions read through the sandbox Hub |
| `read_session_hooks` | Session data readers |
| `delete_chat_session` | `SqliteSessionStore.delete` + file cleanup |
| `update_chat_session_title` | `resolveSessionBackend().updateSession` |
| `delete_chat_session` | `SqliteSessionStore.delete` + file cleanup; cloud sessions also delete the sandbox |
| `update_chat_session_title` | `resolveSessionBackend().updateSession`; cloud sessions PATCH the cloud API |
| `get_feature_flags` | `isCloudAgentsEnabled()` (env override + settings toggle) |
| `get_desktop_settings` | `readDesktopSettings()` |
| `set_cloud_sessions_enabled` | `setCloudSessionsEnabled()` + `feature_flags_changed` broadcast |
| `list_cloud_repositories` | `CloudSessionManager.listRepositories()` (GitHub integration) |
| `list_cloud_branches` | `CloudSessionManager.listBranches()` (paginated) |
| `list_mcp_servers` | Direct file I/O |
| `authorize_mcp_server_oauth` | Explicit Connect action → cancellable `authorizeMcpServerOAuth` + system browser |
| `cancel_mcp_server_oauth` | Cancel the pending MCP OAuth callback wait |
| `upsert_mcp_server` | Direct file I/O |
| `delete_mcp_server` | Direct file I/O |
| `get_git_branch` | async `execFile("git", ...)` |
Realtime mode sessions expose only one browser-callable tool, `run_cline`, when
the selected realtime model supports tool calling. The webview implements that
tool by sending the request through the active Cline chat session and returning
its persisted result to the realtime provider for playback. Cline remains the
owner of workspace context, agent tools, MCP, approvals, and session history;
provider credentials remain in the sidecar.
| `list_git_branches` | async `execFile("git", ...)` |
| `checkout_git_branch` | async `execFile("git", ...)` |
| `search_workspace_files` | `getFileIndex` |
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,56 +0,0 @@
import * as os from "node:os";
import { resolveCoreDistinctId } from "@cline/core";
import type {
ClientContext,
ExtensionContext,
TelemetryMetadata,
UserContext,
} from "@cline/shared";
import { version } from "../package.json";
/** Shared identity for request headers, Hub attribution, and telemetry. */
export const DESKTOP_CLIENT_CONTEXT = {
name: "cline-desktop",
version,
platform: "Cline Desktop",
platformVersion: version,
isMultiRoot: false,
} as const satisfies ClientContext;
export const DESKTOP_TELEMETRY_METADATA = {
extension_version: version,
cline_type: "desktop",
platform: DESKTOP_CLIENT_CONTEXT.platform,
platform_version: DESKTOP_CLIENT_CONTEXT.platformVersion,
os_type: os.platform(),
os_version: os.version(),
} satisfies TelemetryMetadata;
export function resolveDesktopTelemetryUser(input?: {
accountId?: string;
email?: string;
organizationId?: string;
}): UserContext {
const accountId = input?.accountId?.trim();
return accountId
? {
distinctId: accountId,
accountId,
email: input?.email,
organizationId: input?.organizationId,
}
: {
distinctId: resolveCoreDistinctId(),
accountId: null,
};
}
/** Serializable context attached to every Desktop session sent to the Hub. */
export function createDesktopExtensionContext(
user?: UserContext,
): ExtensionContext {
return {
client: DESKTOP_CLIENT_CONTEXT,
...(user ? { user: { ...user } } : {}),
};
}
+14 -11
View File
@@ -1,13 +1,16 @@
import {
captureAuthRefreshSoftFailure,
getProviderAuthHandler,
OAuthReauthRequiredError,
type ProviderSettingsManager,
RuntimeOAuthTokenManager,
} from "@cline/core";
import type { SidecarContext } from "./types";
// Share the refresh-aware manager so single-use refresh tokens stay single-flight.
// Cline access tokens expire between app launches, so account requests must
// resolve through the refresh-aware OAuth manager instead of reading the
// persisted token directly. A single shared instance keeps concurrent account
// requests single-flight; the refresh token is single-use, so parallel
// refreshes would invalidate each other.
let clineOAuthTokenManager: RuntimeOAuthTokenManager | undefined;
export async function resolveFreshClineAuthToken(
@@ -24,13 +27,20 @@ export async function resolveFreshClineAuthToken(
return resolution.apiKey;
}
} catch (error) {
// A persisted token may still let the account request surface the failure.
// Fall back to the persisted token; when one exists the account request
// surfaces the auth failure to the caller.
refreshError = error instanceof Error ? error : new Error(String(error));
}
// Apply canonical OAuth-token formatting while preserving raw API keys.
// The canonical handler applies the same formatting the refresh path uses:
// OAuth access tokens gain the `workos:` prefix core-platform expects,
// while raw API keys pass through untouched.
const persisted = getProviderAuthHandler("cline")?.getApiKey(
manager.getProviderSettings("cline"),
);
// Never-signed-in resolves to undefined without a refresh attempt and is
// silent. A refresh failure with no persisted fallback means credentials
// existed but yielded nothing — that is the signal a real auth regression
// would show up as, so report exactly one event for it.
if (!persisted && refreshError && ctx) {
ctx.logger?.error?.("Cline auth token refresh failed with no fallback", {
error: refreshError,
@@ -40,12 +50,5 @@ export async function resolveFreshClineAuthToken(
errorCode: "desktop_refresh_failed_no_fallback_token",
});
}
// A rejected refresh token means the persisted access token is dead too.
// Handing it out would turn the signed-out state into an opaque request
// failure (an error card whose Retry fails the same way) instead of the
// sign-in prompt. Transient refresh failures still fall back to it.
if (refreshError instanceof OAuthReauthRequiredError) {
return undefined;
}
return persisted;
}
@@ -1,820 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import {
CloudSessionApi,
CloudSessionError,
type CloudSessionRecord,
} from "./cloud-sessions";
const REMOTE_SESSION: CloudSessionRecord = {
id: "ses-outer",
status: "ready",
sandboxUrl: "https://pod.example/hub",
repoContext: { repoUrl: "https://github.com/cline/test" },
metadata: { modelId: "anthropic/claude-sonnet-5" },
createdAt: "2026-08-05T10:00:00.000Z",
updatedAt: "2026-08-05T10:01:00.000Z",
};
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
function jwtFor(subject: string, nonce: string): string {
const encode = (value: unknown) =>
Buffer.from(JSON.stringify(value)).toString("base64url");
return (
"workos:" +
encode({ alg: "none" }) +
"." +
encode({ sub: subject, nonce }) +
".sig"
);
}
describe("CloudSessionApi", () => {
it("resolves a fresh bearer token for every REST request", async () => {
const tokens = ["workos:first", "workos:second"];
const authorizations: string[] = [];
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example/",
appBaseUrl: "https://app.example/",
getAuthToken: async () => tokens.shift(),
fetch: async (_input, init) => {
authorizations.push(
new Headers(init?.headers).get("Authorization") ?? "",
);
return jsonResponse({ success: true, data: [] });
},
});
await api.list();
await api.list();
expect(authorizations).toEqual([
"Bearer workos:first",
"Bearer workos:second",
]);
});
it("uses the dashboard create body and includes branch only when requested", async () => {
const bodies: Array<Record<string, unknown>> = [];
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "sk_test",
fetch: async (_input, init) => {
bodies.push(JSON.parse(String(init?.body)));
return jsonResponse(
{ success: true, data: { sessionId: "ses-1", sandboxUrl: "pod" } },
201,
);
},
});
await api.create({
modelId: "anthropic/claude-sonnet-5",
repoUrl: "https://github.com/cline/test",
branch: "feature/login-fix",
});
await api.create({
modelId: "anthropic/claude-sonnet-5",
repoUrl: "https://github.com/cline/test",
});
expect(bodies[0]).toMatchObject({
modelId: "anthropic/claude-sonnet-5",
repoUrl: "https://github.com/cline/test",
branch: "feature/login-fix",
title: expect.stringMatching(/^__cline_create_request__:/),
});
expect(bodies[1]).toMatchObject({
modelId: "anthropic/claude-sonnet-5",
repoUrl: "https://github.com/cline/test",
title: expect.stringMatching(/^__cline_create_request__:/),
});
expect(bodies[1]).not.toHaveProperty("branch");
});
it("treats a missing history snapshot (404) as null, not an empty archive", async () => {
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "sk_test",
fetch: async () => new Response("not found", { status: 404 }),
});
expect(await api.history("ses-1")).toBeNull();
});
it("accepts v1 history and rejects malformed snapshots instead of returning empty history", async () => {
const messages = [{ role: "user", content: "Hello" }];
let snapshot: unknown = { version: 1, messages };
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "sk_test",
fetch: async () => jsonResponse(snapshot),
});
expect(await api.history("ses-1")).toEqual(messages);
snapshot = { version: 1, messages: [] };
expect(await api.history("ses-1")).toEqual([]);
for (const invalid of [
null,
{ version: 1 },
{ version: 2, messages: [] },
]) {
snapshot = invalid;
await expect(api.history("ses-1")).rejects.toMatchObject({
code: "request_failed",
detail: "Invalid archived session history",
});
}
});
it("returns the real id before polling readiness and reports provisioning phases", async () => {
vi.useFakeTimers();
const tokens = ["workos:create", "workos:create", "workos:new-account"];
const authorizations: string[] = [];
let statusCalls = 0;
const phases: Array<string | undefined> = [];
try {
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => tokens.shift(),
fetch: async (input, init) => {
authorizations.push(
new Headers(init?.headers).get("Authorization") ?? "",
);
const url = new URL(String(input));
if (init?.method === "POST") {
return jsonResponse(
{
success: true,
data: { sessionId: "ses-1", status: "provisioning" },
},
201,
);
}
expect(url.pathname).toBe("/api/v1/session/ses-1/status");
statusCalls += 1;
return jsonResponse({
success: true,
data: {
sessionId: "ses-1",
status: statusCalls === 1 ? "provisioning" : "ready",
phase: statusCalls === 1 ? "cloning_repo" : "ready",
},
});
},
});
const created = await api.create({
modelId: "anthropic/claude-sonnet-5",
repoUrl: "https://github.com/cline/test",
});
expect(created).toMatchObject({
sessionId: "ses-1",
status: "provisioning",
});
expect(statusCalls).toBe(0);
const ready = api.waitUntilReady(
created.sessionId,
new AbortController().signal,
({ phase }) => phases.push(phase),
);
await vi.waitFor(() => expect(statusCalls).toBe(1));
await vi.advanceTimersByTimeAsync(3_000);
await expect(ready).resolves.toBeUndefined();
expect(statusCalls).toBe(2);
expect(phases).toEqual(["cloning_repo", "ready"]);
expect(authorizations).toEqual([
"Bearer workos:create",
"Bearer workos:create",
"Bearer workos:create",
]);
expect(tokens).toEqual(["workos:new-account"]);
} finally {
vi.useRealTimers();
}
});
it("refreshes an expired provisioning token without switching accounts", async () => {
const original = jwtFor("user-1", "original");
const refreshed = jwtFor("user-1", "refreshed");
const tokens = [original, refreshed];
const authorizations: string[] = [];
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => tokens.shift(),
fetch: async (_input, init) => {
const authorization =
new Headers(init?.headers).get("Authorization") ?? "";
authorizations.push(authorization);
if (authorization === `Bearer ${original}`) {
return jsonResponse(
{ success: false, error: "authentication required" },
401,
);
}
return jsonResponse({
success: true,
data: { sessionId: "ses-1", status: "ready" },
});
},
});
await expect(
api.waitUntilReady("ses-1", new AbortController().signal),
).resolves.toBeUndefined();
expect(authorizations).toEqual([
`Bearer ${original}`,
`Bearer ${refreshed}`,
]);
});
it("does not switch accounts while refreshing provisioning auth", async () => {
const original = jwtFor("user-1", "original");
const otherAccount = jwtFor("user-2", "refreshed");
const tokens = [original, otherAccount];
let statusCalls = 0;
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => tokens.shift(),
fetch: async () => {
statusCalls += 1;
return jsonResponse(
{ success: false, error: "authentication required" },
401,
);
},
});
await expect(
api.waitUntilReady("ses-1", new AbortController().signal),
).rejects.toMatchObject({ code: "authentication_required" });
expect(statusCalls).toBe(1);
});
it("returns a recovered real id without waiting for provisioning", async () => {
const requests: string[] = [];
let recoveryTitle = "";
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "workos:fresh",
fetch: async (input, init) => {
requests.push(
`${init?.method ?? "GET"} ${new URL(String(input)).pathname}`,
);
if (init?.method === "POST") {
recoveryTitle = String(JSON.parse(String(init.body)).title);
return jsonResponse({ success: false, error: "gateway" }, 500);
}
return jsonResponse({
success: true,
data: [
{
...REMOTE_SESSION,
id: "ses-recovered",
title: recoveryTitle,
status: "provisioning",
sandboxUrl: "",
},
],
});
},
});
await expect(
api.create({
modelId: "anthropic/claude-sonnet-5",
repoUrl: "https://github.com/cline/test",
}),
).resolves.toMatchObject({
sessionId: "ses-recovered",
status: "provisioning",
});
expect(requests).toEqual(["POST /api/v1/session", "GET /api/v1/session"]);
});
it("recovers the real id after the create request times out", async () => {
vi.useFakeTimers();
const requests: string[] = [];
let recoveryTitle = "";
try {
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
createTimeoutMs: 100,
getAuthToken: async () => "workos:fresh",
fetch: async (input, init) => {
requests.push(
`${init?.method ?? "GET"} ${new URL(String(input)).pathname}`,
);
if (init?.method === "POST") {
recoveryTitle = String(JSON.parse(String(init.body)).title);
return await new Promise<Response>((_resolve, reject) => {
init.signal?.addEventListener(
"abort",
() => reject(init.signal?.reason),
{ once: true },
);
});
}
return jsonResponse({
success: true,
data: [
{
...REMOTE_SESSION,
id: "ses-recovered",
title: recoveryTitle,
status: "provisioning",
sandboxUrl: "",
},
],
});
},
});
const creating = api.create({
modelId: "anthropic/claude-sonnet-5",
repoUrl: "https://github.com/cline/test",
});
await vi.advanceTimersByTimeAsync(100);
await expect(creating).resolves.toMatchObject({
sessionId: "ses-recovered",
});
expect(requests).toEqual(["POST /api/v1/session", "GET /api/v1/session"]);
} finally {
vi.useRealTimers();
}
});
it("returns a failed recovered session without hiding its real id", async () => {
let recoveryTitle = "";
const requests: string[] = [];
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "workos:fresh",
fetch: async (input, init) => {
requests.push(
`${init?.method ?? "GET"} ${new URL(String(input)).pathname}`,
);
if (init?.method === "POST") {
recoveryTitle = String(JSON.parse(String(init.body)).title);
return jsonResponse({ success: false, error: "gateway" }, 500);
}
return jsonResponse({
success: true,
data: [
{
...REMOTE_SESSION,
title: recoveryTitle,
status: "failed",
},
],
});
},
});
await expect(
api.create({
modelId: REMOTE_SESSION.metadata.modelId ?? "",
repoUrl: REMOTE_SESSION.repoContext.repoUrl ?? "",
}),
).resolves.toMatchObject({ sessionId: "ses-outer", status: "failed" });
expect(requests).toEqual(["POST /api/v1/session", "GET /api/v1/session"]);
});
it("recovers a create accepted before a raw network failure", async () => {
let recoveryTitle = "";
let listCalls = 0;
const now = new Date().toISOString();
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "sk_test",
fetch: async (_input, init) => {
if (init?.method === "POST") {
recoveryTitle = String(JSON.parse(String(init.body)).title);
throw new TypeError("fetch failed");
}
listCalls += 1;
return jsonResponse({
success: true,
data: [
{
...REMOTE_SESSION,
id: "ses-recovered",
title: recoveryTitle,
createdAt: now,
updatedAt: now,
},
],
});
},
});
await expect(
api.create({
requestId: "request-a",
modelId: REMOTE_SESSION.metadata.modelId ?? "",
repoUrl: REMOTE_SESSION.repoContext.repoUrl ?? "",
}),
).resolves.toMatchObject({ sessionId: "ses-recovered" });
expect(listCalls).toBe(1);
});
it("does not recover another process's identical session", async () => {
const now = new Date().toISOString();
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "sk_test",
fetch: async (_input, init) =>
init?.method === "POST"
? jsonResponse({ success: false, error: "gateway timeout" }, 500)
: jsonResponse({
success: true,
data: [
{
...REMOTE_SESSION,
id: "ses-other-process",
title: "__cline_create_request__:other-request",
createdAt: now,
updatedAt: now,
},
],
}),
});
await expect(
api.create({
requestId: "this-request",
modelId: REMOTE_SESSION.metadata.modelId ?? "",
repoUrl: REMOTE_SESSION.repoContext.repoUrl ?? "",
}),
).rejects.toMatchObject({ code: "request_failed" });
});
it("hides temporary create request titles from session lists", async () => {
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "sk_test",
fetch: async () =>
jsonResponse({
success: true,
data: [
{
...REMOTE_SESSION,
title: "__cline_create_request__:request-a",
},
],
}),
});
await expect(api.list()).resolves.toEqual([
expect.objectContaining({
id: "ses-outer",
title: undefined,
metadata: expect.objectContaining({
createRequestTitle: "__cline_create_request__:request-a",
}),
}),
]);
});
it("returns a stable, environment-aware GitHub connection error", async () => {
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://staging-app.example/",
getAuthToken: async () => "workos:test",
fetch: async () =>
jsonResponse({ success: false, error: "GitHub is not connected" }, 412),
});
const error = await api
.create({ modelId: "model", repoUrl: "https://github.com/cline/test" })
.catch((caught) => caught);
expect(error).toBeInstanceOf(CloudSessionError);
expect(error.code).toBe("github_not_connected");
expect(error.message).toBe(
'CLOUD_SESSION_ERROR:{"code":"github_not_connected","message":"GitHub is not connected","connectUrl":"https://staging-app.example/dashboard/integrations"}',
);
});
it("routes organization GitHub setup to organization integrations", async () => {
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://staging-app.example/",
getAuthToken: async () => "workos:test",
fetch: async () =>
jsonResponse({ success: false, error: "GitHub is not connected" }, 412),
});
const error = await api
.create({
modelId: "model",
repoUrl: "https://github.com/cline/test",
organizationId: "org-cline-bot",
})
.catch((caught) => caught);
expect(error).toBeInstanceOf(CloudSessionError);
expect(error.connectUrl).toBe(
"https://staging-app.example/dashboard/organization/integrations",
);
});
it("lists connected GitHub repositories and their branches", async () => {
const requestedPaths: string[] = [];
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "workos:test",
fetch: async (input) => {
const path = new URL(String(input)).pathname;
requestedPaths.push(path);
if (path.endsWith("/branches")) {
return jsonResponse({
success: true,
data: [{ name: "main" }, { name: "feature/cloud" }],
});
}
return jsonResponse({
success: true,
data: [
{
id: 42,
name: "cline",
full_name: "cline/cline",
html_url: "https://github.com/cline/cline",
clone_url: "https://github.com/cline/cline.git",
default_branch: "main",
},
],
});
},
});
expect(await api.listRepositories()).toEqual({
connected: true,
connectUrl: "https://app.example/dashboard/integrations",
repositories: [
{
id: 42,
name: "cline",
fullName: "cline/cline",
url: "https://github.com/cline/cline",
defaultBranch: "main",
},
],
});
expect(await api.listBranches(42)).toEqual({
available: true,
branches: ["main", "feature/cloud"],
nextToken: "",
});
expect(requestedPaths).toEqual([
"/api/v1/integrations/github/repositories",
"/api/v1/integrations/github/repositories/42/branches",
]);
});
it("reads paginated branch responses and forwards search cursors", async () => {
let requestedUrl = "";
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "workos:test",
fetch: async (input) => {
requestedUrl = String(input);
return jsonResponse({
success: true,
data: {
items: [{ name: "feature/cloud" }],
nextToken: "next/page",
},
});
},
});
expect(
await api.listBranches(42, undefined, {
cursor: "search cursor",
query: "feature/cloud",
}),
).toEqual({
available: true,
branches: ["feature/cloud"],
nextToken: "next/page",
});
const url = new URL(requestedUrl);
expect(url.pathname).toBe(
"/api/v1/integrations/github/repositories/42/branches",
);
expect(url.searchParams.get("query")).toBe("feature/cloud");
expect(url.searchParams.get("cursor")).toBe("search cursor");
});
it("filters legacy branch responses while backends roll out", async () => {
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "workos:test",
fetch: async () =>
jsonResponse({
success: true,
data: [{ name: "main" }, { name: "feature/cloud" }],
}),
});
expect(await api.listBranches(42, undefined, { query: "FEATURE" })).toEqual(
{
available: true,
branches: ["feature/cloud"],
nextToken: "",
},
);
});
it("falls back to the repository default when the branch API is unavailable", async () => {
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "workos:test",
fetch: async () =>
jsonResponse({ success: false, error: "route not found" }, 404),
});
expect(await api.listBranches(42)).toEqual({
available: false,
branches: [],
});
});
it("uses organization-scoped repository and branch endpoints", async () => {
const requestedPaths: string[] = [];
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "workos:test",
fetch: async (input) => {
const path = new URL(String(input)).pathname;
requestedPaths.push(path);
return jsonResponse({ success: true, data: [] });
},
});
expect(await api.listRepositories("org-cline-bot")).toMatchObject({
connected: true,
connectUrl: "https://app.example/dashboard/organization/integrations",
});
await api.listBranches(42, "org-cline-bot");
expect(requestedPaths).toEqual([
"/api/v1/organizations/org-cline-bot/integrations/github/repositories",
"/api/v1/organizations/org-cline-bot/integrations/github/repositories/42/branches",
]);
});
it("refuses ambiguous recovery for overlapping identical create requests", async () => {
const now = new Date().toISOString();
const record = (id: string, createdAt: string) => ({
id,
title: "__cline_create_request__:same-request",
status: "running",
sandboxUrl: `pod-${id}`,
repoContext: { repoUrl: "https://github.com/cline/test" },
metadata: { modelId: "anthropic/claude-sonnet-5" },
createdAt,
updatedAt: createdAt,
});
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "sk_test",
fetch: async (_input, init) =>
init?.method === "POST"
? jsonResponse({ success: false, error: "gateway timeout" }, 500)
: jsonResponse({
success: true,
data: [
record("ses-newer", now),
record("ses-older", new Date(Date.now() - 1_000).toISOString()),
],
}),
});
const input = {
requestId: "same-request",
modelId: "anthropic/claude-sonnet-5",
repoUrl: "https://github.com/cline/test",
};
const error = await api.create(input).catch((caught) => caught);
expect(error).toMatchObject({ code: "request_failed" });
expect(String(error)).toContain("ambiguous result");
});
it("reports provisioning failure without deleting the known session", async () => {
const authorizations: string[] = [];
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "workos:create",
fetch: async (input, init) => {
authorizations.push(
new Headers(init?.headers).get("Authorization") ?? "",
);
expect(new URL(String(input)).pathname).toBe(
"/api/v1/session/ses-failed/status",
);
return jsonResponse({
success: true,
data: {
sessionId: "ses-failed",
status: "failed",
statusReason: "clone failed",
},
});
},
});
await expect(
api.waitUntilReady("ses-failed", new AbortController().signal),
).rejects.toMatchObject({ code: "session_failed", detail: "clone failed" });
expect(authorizations).toEqual(["Bearer workos:create"]);
});
it("turns a generic forbidden response into actionable account guidance", async () => {
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "workos:test",
fetch: async () =>
jsonResponse({ success: false, error: "forbidden" }, 403),
});
const error = await api
.create({ modelId: "model", repoUrl: "https://github.com/cline/test" })
.catch((caught) => caught);
expect(error).toBeInstanceOf(CloudSessionError);
expect(error.code).toBe("request_failed");
expect(error.status).toBe(403);
expect(error.message).toContain(
"Switch to Personal or another organization in Settings → Account",
);
});
it("does not run list recovery after a fast client-side rejection", async () => {
let listRequests = 0;
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example",
getAuthToken: async () => "sk_test",
fetch: async (_input, init) => {
if (init?.method === "POST") {
return jsonResponse({ success: false, error: "invalid branch" }, 422);
}
listRequests += 1;
return jsonResponse({ success: true, data: [] });
},
});
await expect(
api.create({
modelId: "anthropic/claude-sonnet-5",
repoUrl: "https://github.com/cline/test",
}),
).rejects.toThrow(/invalid branch/);
expect(listRequests).toBe(0);
});
it("returns the GitHub connection action when no integration exists", async () => {
const api = new CloudSessionApi({
apiBaseUrl: "https://api.example",
appBaseUrl: "https://app.example/",
getAuthToken: async () => "workos:test",
fetch: async () =>
jsonResponse({ success: false, error: "not connected" }, 404),
});
expect(await api.listRepositories()).toEqual({
connected: false,
connectUrl: "https://app.example/dashboard/integrations",
repositories: [],
});
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -6,19 +6,24 @@ const clineAccountServiceCtorMock = vi.hoisted(() => vi.fn());
const executeClineAccountActionMock = vi.hoisted(() => vi.fn());
const getProviderSettingsMock = vi.hoisted(() => vi.fn());
const saveProviderSettingsMock = vi.hoisted(() => vi.fn());
const persistProviderSettingsMock = vi.hoisted(() => vi.fn());
const resolveProviderApiKeyMock = vi.hoisted(() => vi.fn());
const clearLegacyProviderCredentialsMock = vi.hoisted(() => vi.fn());
vi.mock("./legacy-provider-credentials", () => ({
clearLegacyProviderCredentials: clearLegacyProviderCredentialsMock,
}));
vi.mock("@cline/core", async () => {
const actual =
await vi.importActual<typeof import("@cline/core")>("@cline/core");
return {
...actual,
ClientSettingsManager: class {
initializeModesIfMissing() {
return { version: 1, modes: {} };
}
read() {
return { version: 1, modes: {} };
}
setModeSettings() {
return { version: 1, modes: {} };
}
},
ClineAccountService: class {
constructor(options: unknown) {
clineAccountServiceCtorMock(options);
@@ -27,7 +32,9 @@ vi.mock("@cline/core", async () => {
executeClineAccountAction: executeClineAccountActionMock,
ProviderSettingsManager: class {
getProviderSettings = getProviderSettingsMock;
saveProviderSettings = persistProviderSettingsMock;
read() {
return { modes: {} };
}
},
saveLocalProviderSettings: saveProviderSettingsMock,
RuntimeOAuthTokenManager: class {
@@ -38,13 +45,12 @@ vi.mock("@cline/core", async () => {
function createContext() {
const capture = vi.fn();
const setDistinctId = vi.fn();
const updateCommonProperties = vi.fn();
const ctx = {
telemetry: { capture, setDistinctId, updateCommonProperties },
telemetry: { capture },
logger: { debug: vi.fn(), log: vi.fn(), error: vi.fn() },
wsClients: new Set(),
} as unknown as SidecarContext;
return { ctx, capture, setDistinctId, updateCommonProperties };
return { ctx, capture };
}
const FETCH_ME_ARGS = {
@@ -62,15 +68,12 @@ beforeEach(() => {
executeClineAccountActionMock.mockReset();
getProviderSettingsMock.mockReset();
saveProviderSettingsMock.mockReset();
persistProviderSettingsMock.mockReset();
resolveProviderApiKeyMock.mockReset();
clearLegacyProviderCredentialsMock.mockReset();
});
describe("cline_account command auth states", () => {
it("returns a typed not-authenticated result and restores anonymous telemetry when signed out", async () => {
const { ctx, capture, setDistinctId, updateCommonProperties } =
createContext();
it("returns a typed not-authenticated result when signed out, without telemetry or a thrown error", async () => {
const { ctx, capture } = createContext();
resolveProviderApiKeyMock.mockResolvedValue(null);
getProviderSettingsMock.mockReturnValue(undefined);
@@ -84,14 +87,6 @@ describe("cline_account command auth states", () => {
expect(executeClineAccountActionMock).not.toHaveBeenCalled();
expect(clineAccountServiceCtorMock).not.toHaveBeenCalled();
expect(capture).not.toHaveBeenCalled();
expect(setDistinctId).toHaveBeenCalledWith(expect.any(String));
expect(updateCommonProperties).toHaveBeenCalledWith(
expect.objectContaining({
user_id: undefined,
account_id: undefined,
organization_id: undefined,
}),
);
});
it("runs the account action unchanged when a fresh token resolves", async () => {
@@ -133,31 +128,14 @@ describe("cline_account command auth states", () => {
const serviceOptions = clineAccountServiceCtorMock.mock.calls[0][0] as {
getAuthToken: () => Promise<string | undefined>;
};
// Persisted OAuth tokens gain the `workos:` prefix required by
// core-platform (see cline-auth.ts).
await expect(serviceOptions.getAuthToken()).resolves.toBe(
"workos:persisted-token",
);
expect(capture).not.toHaveBeenCalled();
});
it("reports signed out when the refresh token is rejected even though a stale access token is persisted", async () => {
// The stale token would only fail the account request with a 401,
// which rendered an error card whose Retry failed the same way.
const { ctx } = createContext();
const { OAuthReauthRequiredError } =
await vi.importActual<typeof import("@cline/core")>("@cline/core");
resolveProviderApiKeyMock.mockRejectedValue(
new OAuthReauthRequiredError("cline"),
);
getProviderSettingsMock.mockReturnValue({
auth: { accessToken: "persisted-token" },
});
const result = await runClineAccountCommand(ctx);
expect(isClineAccountNotAuthenticatedResult(result)).toBe(true);
expect(executeClineAccountActionMock).not.toHaveBeenCalled();
});
it("reports one auth refresh soft-failure event when the refresh fails and no fallback token exists", async () => {
const { ctx, capture } = createContext();
const refreshError = new Error(
@@ -210,7 +188,7 @@ describe("cline_account keeps feature-flag identity in sync", () => {
});
it("adopts the account identity on login", async () => {
const { ctx, setDistinctId, updateCommonProperties } = createContext();
const { ctx } = createContext();
resolveProviderApiKeyMock.mockResolvedValue({ apiKey: "token" });
getProviderSettingsMock.mockReturnValue({});
executeClineAccountActionMock.mockResolvedValue({
@@ -221,62 +199,6 @@ describe("cline_account keeps feature-flag identity in sync", () => {
await runOperation(ctx, "fetchMe");
expect(await currentFlagsUserId()).toBe("acct-1");
expect(setDistinctId).toHaveBeenCalledWith("acct-1");
expect(updateCommonProperties).toHaveBeenCalledWith(
expect.objectContaining({ user_id: "acct-1", account_id: "acct-1" }),
);
expect(ctx.telemetryUser).toEqual({
distinctId: "acct-1",
accountId: "acct-1",
email: "dev@example.com",
organizationId: undefined,
});
});
it("applies and persists the active organization for task telemetry", async () => {
const { ctx, updateCommonProperties } = createContext();
resolveProviderApiKeyMock.mockResolvedValue({ apiKey: "token" });
getProviderSettingsMock.mockReturnValue({
provider: "cline",
auth: { accountId: "acct-1", accessToken: "token" },
});
executeClineAccountActionMock.mockResolvedValue({
id: "acct-1",
email: "dev@example.com",
organizations: [
{
active: true,
memberId: "member-1",
name: "Acme",
organizationId: "org-1",
roles: ["member"],
},
],
});
await runOperation(ctx, "fetchMe");
expect(updateCommonProperties).toHaveBeenCalledWith(
expect.objectContaining({
user_id: "acct-1",
organization_id: "org-1",
}),
);
expect(persistProviderSettingsMock).toHaveBeenCalledWith(
expect.objectContaining({
auth: expect.objectContaining({
organizationId: "org-1",
memberId: "member-1",
}),
}),
{ setLastUsed: false },
);
expect(ctx.telemetryUser).toEqual({
distinctId: "acct-1",
accountId: "acct-1",
email: "dev@example.com",
organizationId: "org-1",
});
});
it("leaves the signed-in identity intact across an organization switch", async () => {
@@ -314,7 +236,7 @@ describe("cline_account keeps feature-flag identity in sync", () => {
});
it("clears the account identity on logout", async () => {
const { ctx, setDistinctId, updateCommonProperties } = createContext();
const { ctx } = createContext();
resolveProviderApiKeyMock.mockResolvedValue({ apiKey: "token" });
getProviderSettingsMock.mockReturnValue({});
executeClineAccountActionMock.mockResolvedValue({ id: "acct-1" });
@@ -328,28 +250,10 @@ describe("cline_account keeps feature-flag identity in sync", () => {
await runOperation(ctx, "fetchMe");
expect(await currentFlagsUserId()).toBeUndefined();
expect(ctx.telemetryUser).toEqual(
expect.objectContaining({
accountId: null,
distinctId: expect.any(String),
}),
);
expect(setDistinctId).toHaveBeenLastCalledWith(
ctx.telemetryUser?.distinctId,
);
expect(ctx.telemetryUser?.distinctId).not.toBe("acct-1");
expect(updateCommonProperties).toHaveBeenLastCalledWith(
expect.objectContaining({
user_id: undefined,
account_id: undefined,
account_email: undefined,
organization_id: undefined,
}),
);
});
it("clears the identity when sign-out blanks the cline auth settings", async () => {
const { ctx, setDistinctId, updateCommonProperties } = createContext();
const { ctx } = createContext();
resolveProviderApiKeyMock.mockResolvedValue({ apiKey: "token" });
getProviderSettingsMock.mockReturnValue({});
executeClineAccountActionMock.mockResolvedValue({ id: "acct-1" });
@@ -372,47 +276,6 @@ describe("cline_account keeps feature-flag identity in sync", () => {
});
expect(await currentFlagsUserId()).toBeUndefined();
expect(ctx.telemetryUser).toEqual(
expect.objectContaining({
accountId: null,
distinctId: expect.any(String),
}),
);
expect(setDistinctId).toHaveBeenLastCalledWith(
ctx.telemetryUser?.distinctId,
);
expect(updateCommonProperties).toHaveBeenLastCalledWith(
expect.objectContaining({
user_id: undefined,
account_id: undefined,
organization_id: undefined,
}),
);
});
it("signs out of the shared cline entry and legacy secrets when cline-pass is disabled", async () => {
const { ctx } = createContext();
getProviderSettingsMock.mockReturnValue(undefined);
saveProviderSettingsMock.mockImplementation(
(_manager: unknown, request: { providerId: string }) => ({
providerId: request.providerId,
enabled: false,
settingsPath: "/tmp/settings.json",
}),
);
const { handleCommand } = await import("./commands");
await handleCommand(ctx, "save_provider_settings", {
provider: "cline-pass",
enabled: false,
});
// Cline Pass stores its credentials under "cline", so both entries go,
// and the legacy secrets are cleared for the storage provider.
expect(saveProviderSettingsMock.mock.calls.map(([, r]) => r)).toEqual([
expect.objectContaining({ providerId: "cline-pass", enabled: false }),
{ providerId: "cline", enabled: false },
]);
expect(clearLegacyProviderCredentialsMock).toHaveBeenCalledWith("cline");
});
it("ignores settings writes for other providers", async () => {
@@ -440,6 +303,9 @@ describe("cline_account keeps feature-flag identity in sync", () => {
it("falls back to the device distinct ID after logout", async () => {
const { ctx } = createContext();
const { getDesktopFeatureFlagsContext } = await import("./feature-flags");
resolveProviderApiKeyMock.mockResolvedValue(null);
getProviderSettingsMock.mockReturnValue(undefined);
await runOperation(ctx, "fetchMe");
const deviceId = getDesktopFeatureFlagsContext().distinctId;
resolveProviderApiKeyMock.mockResolvedValue({ apiKey: "token" });
@@ -14,7 +14,7 @@ vi.mock("@cline/core", async () => {
function createContext(): SidecarContext {
return {
workspaceRoot: "/workspace",
localWorkspaceRoot: "/workspace",
wsClients: new Set(),
hubBuildMismatch: {
url: "ws://127.0.0.1:25463/hub",
@@ -0,0 +1,149 @@
import type { ProviderModesSettings } from "@cline/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { SidecarContext } from "./types";
const coreMocks = vi.hoisted(() => ({
ensureCustomProvidersLoaded: vi.fn(),
initializeModesIfMissing: vi.fn(),
listLocalProviders: vi.fn(),
providerRead: vi.fn(),
readClientSettings: vi.fn(),
saveLocalProviderSettings: vi.fn(),
saveModeSettings: vi.fn(),
setModeSettings: vi.fn(),
}));
vi.mock("@cline/core", async () => {
const actual =
await vi.importActual<typeof import("@cline/core")>("@cline/core");
return {
...actual,
ClientSettingsManager: class {
initializeModesIfMissing = coreMocks.initializeModesIfMissing;
read = coreMocks.readClientSettings;
setModeSettings = coreMocks.setModeSettings;
},
ensureCustomProvidersLoaded: coreMocks.ensureCustomProvidersLoaded,
listLocalProviders: coreMocks.listLocalProviders,
ProviderSettingsManager: class {
read = coreMocks.providerRead;
},
saveLocalProviderSettings: coreMocks.saveLocalProviderSettings,
saveModeSettings: coreMocks.saveModeSettings,
};
});
import { handleCommand } from "./commands";
function createContext(): SidecarContext {
return {
logger: { debug: vi.fn(), error: vi.fn(), log: vi.fn() },
wsClients: new Set(),
} as unknown as SidecarContext;
}
let storedModes: ProviderModesSettings;
let clientSettingsInitialized: boolean;
beforeEach(() => {
storedModes = {};
clientSettingsInitialized = false;
for (const mock of Object.values(coreMocks)) mock.mockReset();
coreMocks.providerRead.mockReturnValue({ modes: {} });
coreMocks.initializeModesIfMissing.mockImplementation(
(modes: ProviderModesSettings) => {
if (!clientSettingsInitialized) {
storedModes = { ...modes };
clientSettingsInitialized = true;
}
return { version: 1, modes: storedModes };
},
);
coreMocks.readClientSettings.mockImplementation(() => ({
version: 1,
modes: storedModes,
}));
coreMocks.setModeSettings.mockImplementation(
(mode: keyof ProviderModesSettings, settings: unknown) => {
storedModes = { ...storedModes };
if (settings) storedModes[mode] = settings as never;
else delete storedModes[mode];
return { version: 1, modes: storedModes };
},
);
coreMocks.saveModeSettings.mockImplementation(
async (
_manager: unknown,
request: { mode: keyof ProviderModesSettings; settings?: unknown },
store: { setModeSettings: (mode: string, settings: unknown) => unknown },
) => {
store.setModeSettings(request.mode, request.settings);
return { settingsPath: "/tmp/client-settings.json", modes: storedModes };
},
);
coreMocks.listLocalProviders.mockImplementation(
async (
_manager: unknown,
options: { modeSettings: ProviderModesSettings },
) => ({ providers: [], modes: options.modeSettings }),
);
});
describe("desktop provider mode commands", () => {
it("persists Voice settings in the same client store used by the catalog", async () => {
const ctx = createContext();
const selection = { providerId: "groq", modelId: "whisper-large-v3" };
const saved = await handleCommand(ctx, "save_voice_input_settings", {
provider: selection.providerId,
model: selection.modelId,
});
const catalog = (await handleCommand(ctx, "list_provider_catalog")) as {
modes: ProviderModesSettings;
};
expect(coreMocks.saveModeSettings).toHaveBeenCalledWith(
expect.anything(),
{ mode: "voiceInput", settings: selection },
expect.objectContaining({
read: coreMocks.readClientSettings,
setModeSettings: coreMocks.setModeSettings,
}),
);
expect(saved).toMatchObject({ voiceInput: selection });
expect(catalog.modes.voiceInput).toEqual(selection);
});
it("durably clears every mode that references a disconnected provider", async () => {
const legacyModes: ProviderModesSettings = {
voiceInput: { providerId: "openai", modelId: "whisper-1" },
voiceOutput: { providerId: "openai", modelId: "gpt-4o-mini-tts" },
realtimeVoice: { providerId: "openai", modelId: "gpt-realtime" },
};
coreMocks.providerRead.mockReturnValue({ modes: legacyModes });
coreMocks.saveLocalProviderSettings.mockReturnValue({
providerId: "openai",
enabled: false,
settingsPath: "/tmp/providers.json",
});
await handleCommand(createContext(), "save_provider_settings", {
provider: "openai",
enabled: false,
});
const catalog = (await handleCommand(
createContext(),
"list_provider_catalog",
)) as { modes: ProviderModesSettings };
expect(coreMocks.setModeSettings.mock.calls).toEqual([
["voiceInput", undefined],
["voiceOutput", undefined],
["realtimeVoice", undefined],
]);
expect(coreMocks.initializeModesIfMissing).toHaveBeenCalledWith(
legacyModes,
);
expect(catalog.modes).toEqual({});
});
});
@@ -0,0 +1,131 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { handleCommand } from "./commands";
import type { SidecarContext } from "./types";
function createContext(): {
ctx: SidecarContext;
events: Array<{ name: string; payload: Record<string, unknown> }>;
} {
const events: Array<{ name: string; payload: Record<string, unknown> }> = [];
const ctx = {
liveSessions: new Map(),
restoringWorkspacePaths: new Set(),
streamIndices: new Map(),
wsClients: new Set([
{
send(message: string) {
const parsed = JSON.parse(message) as {
event: { name: string; payload: Record<string, unknown> };
};
events.push(parsed.event);
},
},
]),
pendingApprovals: new Map(),
pendingQuestions: new Map(),
sessionManager: null,
hubClient: null,
workspaceRoot: "/local/workspace",
unsubscribeSessionEvents: null,
cloudSessionManager: null,
} as unknown as SidecarContext;
return { ctx, events };
}
let dataDir: string;
beforeEach(() => {
dataDir = mkdtempSync(join(tmpdir(), "cline-commands-settings-"));
process.env.CLINE_DATA_DIR = dataDir;
});
afterEach(() => {
delete process.env.CLINE_CODE_CLOUD_AGENTS;
delete process.env.CLINE_DATA_DIR;
rmSync(dataDir, { recursive: true, force: true });
});
describe("desktop settings commands", () => {
it("reads default desktop settings and an off feature gate", async () => {
const { ctx } = createContext();
await expect(
handleCommand(ctx, "get_desktop_settings", {}),
).resolves.toEqual({ cloudSessionsEnabled: false });
await expect(handleCommand(ctx, "get_feature_flags", {})).resolves.toEqual({
cloudAgents: false,
flags: {
"code-onboarding-github": false,
"ext-cline-pass": false,
"internal-composio-connectors": false,
},
});
});
it("rejects a non-boolean cloud sessions toggle value", async () => {
const { ctx, events } = createContext();
await expect(
handleCommand(ctx, "set_cloud_sessions_enabled", {
cloud_sessions_enabled: "yes",
}),
).rejects.toThrow("cloud_sessions_enabled must be a boolean");
expect(events).toEqual([]);
});
it("persists the toggle and broadcasts the new gate immediately", async () => {
const { ctx, events } = createContext();
await expect(
handleCommand(ctx, "set_cloud_sessions_enabled", {
cloud_sessions_enabled: true,
}),
).resolves.toEqual({ cloudSessionsEnabled: true });
// Open webviews re-evaluate without waiting for a restart or account
// change.
expect(events).toEqual([
{
name: "feature_flags_changed",
payload: { cloudAgents: true },
},
]);
await expect(handleCommand(ctx, "get_feature_flags", {})).resolves.toEqual({
cloudAgents: true,
flags: {
"code-onboarding-github": false,
"ext-cline-pass": false,
"internal-composio-connectors": false,
},
});
await handleCommand(ctx, "set_cloud_sessions_enabled", {
cloud_sessions_enabled: false,
});
expect(events.at(-1)).toEqual({
name: "feature_flags_changed",
payload: { cloudAgents: false },
});
});
it("reports the env override through the feature gate", async () => {
const { ctx } = createContext();
process.env.CLINE_CODE_CLOUD_AGENTS = "1";
await expect(handleCommand(ctx, "get_feature_flags", {})).resolves.toEqual({
cloudAgents: true,
flags: {
"code-onboarding-github": false,
"ext-cline-pass": false,
"internal-composio-connectors": false,
},
});
// The toggle's stored value is reported as-is; the override only
// affects the effective gate.
await expect(
handleCommand(ctx, "get_desktop_settings", {}),
).resolves.toEqual({ cloudSessionsEnabled: false });
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+182 -292
View File
@@ -26,6 +26,11 @@ vi.mock("@cline/core", async () => {
await vi.importActual<typeof import("@cline/core")>("@cline/core");
return {
...actual,
ClientSettingsManager: class {
initializeModesIfMissing = vi.fn();
read = vi.fn(() => ({ modes: {} }));
getModeSettings = vi.fn(() => undefined);
},
ClineCore: {
create: createCoreMock,
},
@@ -194,7 +199,7 @@ describe("Code sidecar runtime capabilities", () => {
const ctx = createSidecarContext("/workspace/project");
const hubClient = await ensureSharedHubClient(ctx);
expect(hubClient).toBe(ctx.hubClient);
expect(hubClient).toBeDefined();
expect(ensureCompatibleLocalHubUrlMock).toHaveBeenCalledWith({
strategy: "require-hub",
@@ -230,8 +235,13 @@ describe("Code sidecar runtime capabilities", () => {
];
const command = vi.fn(async () => ({ ok: true, payload: { hits } }));
const list = vi.fn(async () => []);
ctx.hubClient = { command } as never;
ctx.sessionManager = { list } as never;
ctx.runtimeBindings.set("local", {
environmentId: "local",
kind: "local",
workspaceRoot: "/workspace/project",
hubClient: { command },
sessionManager: { list },
} as never);
const results = (await handleCommand(ctx, "search_sessions", {
query: "generate",
@@ -269,8 +279,13 @@ describe("Code sidecar runtime capabilities", () => {
metadata: { title: oversizedPrompt },
},
]);
ctx.hubClient = { command } as never;
ctx.sessionManager = { list } as never;
ctx.runtimeBindings.set("local", {
environmentId: "local",
kind: "local",
workspaceRoot: "/workspace/project",
hubClient: { command },
sessionManager: { list },
} as never);
const results = (await handleCommand(ctx, "search_sessions", {
query: "generate",
@@ -304,8 +319,13 @@ describe("Code sidecar runtime capabilities", () => {
metadata: { title: "generate an image of a puppy" },
},
]);
ctx.hubClient = { command } as never;
ctx.sessionManager = { list } as never;
ctx.runtimeBindings.set("local", {
environmentId: "local",
kind: "local",
workspaceRoot: "/workspace/project",
hubClient: { command },
sessionManager: { list },
} as never);
const results = (await handleCommand(ctx, "search_sessions", {
query: "generate",
@@ -339,8 +359,13 @@ describe("Code sidecar runtime capabilities", () => {
metadata: { title: "generate an image of a puppy" },
},
]);
ctx.hubClient = { command } as never;
ctx.sessionManager = { list } as never;
ctx.runtimeBindings.set("local", {
environmentId: "local",
kind: "local",
workspaceRoot: "/workspace/project",
hubClient: { command },
sessionManager: { list },
} as never);
const pending = handleCommand(ctx, "search_sessions", {
query: "generate",
@@ -361,7 +386,7 @@ describe("Code sidecar runtime capabilities", () => {
}
});
it("forwards raw hub tool updates to attached desktop sessions", async () => {
it("leaves raw hub tool updates to the canonical Core event stream", async () => {
const { createSidecarContext, handleHubLiveEvent } = await import(
"./context"
);
@@ -387,25 +412,7 @@ describe("Code sidecar runtime capabilities", () => {
},
});
const forwarded = readEvents(ctx).find(
(message) =>
message.event.name === "chat_event" &&
(message.event.payload as { stream?: string }).stream ===
"chat_tool_call_update",
);
expect(forwarded?.event.payload).toMatchObject({
sessionId: "session-1",
stream: "chat_tool_call_update",
});
expect(
JSON.parse(
String((forwarded?.event.payload as { chunk?: string }).chunk),
),
).toEqual({
toolCallId: "call-1",
toolName: "run_commands",
update: { stream: "stdout", chunk: "live\n" },
});
expect(readEvents(ctx)).toEqual([]);
});
it("forwards proceed-while-running requests to the hub", async () => {
@@ -453,6 +460,44 @@ describe("Code sidecar runtime capabilities", () => {
});
});
it("leaves attached-session content projection to the Core event stream", async () => {
const { createSidecarContext, handleHubLiveEvent } = await import(
"./context"
);
const ctx = createSidecarContext("/workspace/project");
ctx.wsClients.add({ send: vi.fn() });
ctx.liveSessions.set("session-image", {
config: {},
messages: [],
promptsInQueue: [],
busy: true,
startedAt: Date.now(),
status: "running",
attachedViaHub: true,
});
for (const event of [
"assistant.delta",
"assistant.image",
"reasoning.delta",
"tool.started",
"tool.updated",
"tool.finished",
]) {
handleHubLiveEvent(ctx, {
event,
sessionId: "session-image",
payload: {
text: "one canonical copy",
toolCallId: "tool-1",
toolName: "run_commands",
},
});
}
expect(readEvents(ctx)).toEqual([]);
});
it("announces a queued prompt start once when drain emits both queue events", async () => {
const { createSidecarContext, initializeSessionManager } = await import(
"./context"
@@ -531,65 +576,6 @@ describe("Code sidecar runtime capabilities", () => {
).toHaveLength(2);
});
it("does not announce a queued prompt start when the head is deleted from the queue", async () => {
const { createSidecarContext, initializeSessionManager } = await import(
"./context"
);
let onEvent: ((event: unknown) => void) | undefined;
createCoreMock.mockResolvedValue({
runtimeAddress: "ws://127.0.0.1:25463/hub",
subscribe: vi.fn((handler: (event: unknown) => void) => {
onEvent = handler;
return () => {};
}),
dispose: vi.fn(),
});
const ctx = createSidecarContext("/workspace/project");
ctx.wsClients.add({ send: vi.fn() });
await initializeSessionManager(ctx);
ctx.liveSessions.set("session-1", {
config: {},
messages: [],
promptsInQueue: [
{ id: "prompt-1", prompt: "first", steer: false, attachmentCount: 0 },
{ id: "prompt-2", prompt: "second", steer: false, attachmentCount: 0 },
],
busy: true,
startedAt: Date.now(),
status: "running",
});
// Removing the head only produces a shrunken snapshot — no
// pending_prompt_submitted — so nothing must reach the transcript.
onEvent?.({
type: "pending_prompts",
payload: {
sessionId: "session-1",
prompts: [{ id: "prompt-2", prompt: "second", delivery: "queue" }],
},
});
const events = readEvents(ctx);
expect(
events.filter(
(message) =>
message.event.name === "chat_event" &&
(message.event.payload as { stream?: string }).stream ===
"chat_queued_prompt_start",
),
).toHaveLength(0);
expect(
events.find((message) => message.event.name === "prompts_in_queue_state")
?.event.payload,
).toEqual({
sessionId: "session-1",
items: [
{ id: "prompt-2", prompt: "second", steer: false, attachmentCount: 0 },
],
});
});
it("relays generated media for attach-only Hub sessions", async () => {
const { createSidecarContext, handleHubLiveEvent } = await import(
"./context"
@@ -670,6 +656,43 @@ describe("Code sidecar runtime capabilities", () => {
expect(readEvents(ctx)).toEqual([]);
});
it("does not treat a resident Hub process as an active turn after attach", async () => {
const { createSidecarContext, handleHubLiveEvent } = await import(
"./context"
);
const ctx = createSidecarContext("/workspace/project");
ctx.liveSessions.set("forked-session", {
config: {},
messages: [],
promptsInQueue: [],
busy: false,
startedAt: Date.now(),
status: "idle",
attachedViaHub: true,
});
handleHubLiveEvent(ctx, {
event: "session.updated",
sessionId: "forked-session",
payload: { session: { status: "running" } },
});
expect(ctx.liveSessions.get("forked-session")).toMatchObject({
status: "idle",
busy: false,
});
handleHubLiveEvent(ctx, {
event: "run.started",
sessionId: "forked-session",
});
expect(ctx.liveSessions.get("forked-session")).toMatchObject({
status: "running",
busy: true,
});
});
it("resolves askQuestion through the websocket request/response protocol", async () => {
const { createSidecarContext, initializeSessionManager } = await import(
"./context"
@@ -883,6 +906,52 @@ describe("Code sidecar runtime capabilities", () => {
).toEqual([]);
});
it("keeps an approval visible when its remote acknowledgement fails", async () => {
const { createSidecarContext } = await import("./context");
const { handleCommand } = await import("./commands");
const ctx = createSidecarContext("/workspace/project");
const approvalClient = {
data: { canApproveTools: true },
send: vi.fn(),
};
ctx.wsClients.add(approvalClient);
// Cloud-session approvals are relayed from a pod without a local
// owner; any trusted surface may answer them.
ctx.pendingApprovals.set("cloud-approval", {
item: {
requestId: "cloud-approval",
sessionId: "ses-cloud",
createdAt: new Date().toISOString(),
toolCallId: "tool-1",
toolName: "run_commands",
},
resolve: async () => {
throw new Error("hub disconnected");
},
});
await expect(
handleCommand(
ctx,
"respond_tool_approval",
{
sessionId: "ses-cloud",
requestId: "cloud-approval",
approved: true,
},
{ connection: approvalClient },
),
).rejects.toThrow("hub disconnected");
expect(
await handleCommand(
ctx,
"poll_tool_approvals",
{ sessionId: "ses-cloud" },
{ connection: approvalClient },
),
).toEqual([expect.objectContaining({ requestId: "cloud-approval" })]);
});
it("rejects and removes an approval when initial delivery fails", async () => {
const { createSidecarContext, createSidecarRuntimeCapabilities } =
await import("./context");
@@ -1329,216 +1398,37 @@ describe("disposeSidecarContext attachment cleanup", () => {
expect(existsSync(queuedFile)).toBe(false);
expect(ctx.liveSessions.size).toBe(0);
});
});
describe("Chat chunk pipe selection", () => {
async function createStreamingContext(
sessionId: string,
coreSubscriptions: Set<string> = new Set(),
) {
const { createSidecarContext } = await import("./context");
it("waits for pending approval callbacks before shutdown completes", async () => {
const { createSidecarContext, disposeSidecarContext } = await import(
"./context"
);
const ctx = createSidecarContext("/workspace/project");
ctx.wsClients.add({ send: vi.fn() });
ctx.liveSessions.set(sessionId, {
config: {},
messages: [],
promptsInQueue: [],
busy: true,
startedAt: Date.now(),
status: "running",
attachedViaHub: true,
});
ctx.sessionManager = {
hasSessionSubscription: (id: string) => coreSubscriptions.has(id),
} as never;
return ctx;
}
function coreTextEvent(sessionId: string, text: string) {
return {
type: "agent_event",
payload: {
sessionId,
event: { type: "content_start", contentType: "text", text },
},
} as never;
}
function eventsFor(ctx: SidecarContext, name: string) {
return readEvents(ctx)
.filter((message) => message.event.name === name)
.map((message) => message.event.payload);
}
function chunksFor(ctx: SidecarContext, stream: string): string[] {
return eventsFor(ctx, "chat_event")
.filter((payload) => (payload as { stream?: string }).stream === stream)
.map((payload) => String((payload as { chunk?: string }).chunk));
}
it("emits one copy when both pipes carry the same delta", async () => {
const { handleCoreSessionEvent, handleHubLiveEvent } = await import(
"./context"
);
// Opening a session arms both pipes: ClineCore subscribes to the session
// and `attach` enables the observer projection, so the hub publishes each
// delta to both sockets.
const ctx = await createStreamingContext(
"session-1",
new Set(["session-1"]),
);
handleHubLiveEvent(ctx, {
event: "assistant.delta",
sessionId: "session-1",
payload: { text: "Pack " },
});
handleCoreSessionEvent(ctx, coreTextEvent("session-1", "Pack "));
handleHubLiveEvent(ctx, {
event: "assistant.delta",
sessionId: "session-1",
payload: { text: "my box" },
});
handleCoreSessionEvent(ctx, coreTextEvent("session-1", "my box"));
expect(chunksFor(ctx, "chat_text")).toEqual(["Pack ", "my box"]);
});
it("still streams sessions only the observer delivers", async () => {
const { handleHubLiveEvent } = await import("./context");
const ctx = await createStreamingContext("session-1");
handleHubLiveEvent(ctx, {
event: "assistant.delta",
sessionId: "session-1",
payload: { text: "remote " },
});
handleHubLiveEvent(ctx, {
event: "assistant.delta",
sessionId: "session-1",
payload: { text: "run" },
});
expect(chunksFor(ctx, "chat_text")).toEqual(["remote ", "run"]);
});
it("mutes the whole observer projection, not just text", async () => {
const { handleHubLiveEvent } = await import("./context");
const ctx = await createStreamingContext(
"session-1",
new Set(["session-1"]),
);
handleHubLiveEvent(ctx, {
event: "tool.started",
sessionId: "session-1",
payload: { toolCallId: "call-1", toolName: "run_commands" },
});
handleHubLiveEvent(ctx, {
event: "run.completed",
sessionId: "session-1",
payload: {},
});
expect(chunksFor(ctx, "chat_tool_call_start")).toEqual([]);
expect(eventsFor(ctx, "chat_session_ended")).toEqual([]);
expect(ctx.liveSessions.get("session-1")?.busy).toBe(true);
});
it("follows the subscription as it comes and goes", async () => {
const { handleHubLiveEvent } = await import("./context");
const coreSubscriptions = new Set<string>();
const ctx = await createStreamingContext("session-1", coreSubscriptions);
const delta = (text: string) =>
handleHubLiveEvent(ctx, {
event: "assistant.delta",
let release: (() => void) | undefined;
ctx.pendingApprovals.set("approval-1", {
item: {
requestId: "approval-1",
sessionId: "session-1",
payload: { text },
});
delta("observer first");
// A send (or pending-prompt list) subscribes ClineCore.
coreSubscriptions.add("session-1");
delta("muted");
// `stop` drops the subscription; a run another client starts on the
// same session is the observer's to render again.
coreSubscriptions.delete("session-1");
delta("observer again");
expect(chunksFor(ctx, "chat_text")).toEqual([
"observer first",
"observer again",
]);
});
it("decides per session", async () => {
const { handleHubLiveEvent } = await import("./context");
const ctx = await createStreamingContext(
"session-1",
new Set(["session-1"]),
);
ctx.liveSessions.set("session-2", {
config: {},
messages: [],
promptsInQueue: [],
busy: true,
startedAt: Date.now(),
status: "running",
attachedViaHub: true,
createdAt: new Date().toISOString(),
toolCallId: "tool-1",
toolName: "run_commands",
input: {},
},
resolve: async () =>
await new Promise<void>((resolve) => {
release = resolve;
}),
});
handleHubLiveEvent(ctx, {
event: "assistant.delta",
sessionId: "session-1",
payload: { text: "one" },
});
handleHubLiveEvent(ctx, {
event: "assistant.delta",
sessionId: "session-2",
payload: { text: "two" },
let disposed = false;
const disposing = disposeSidecarContext(ctx, "test_shutdown").then(() => {
disposed = true;
});
await vi.waitFor(() => expect(release).toBeTypeOf("function"));
expect(disposed).toBe(false);
expect(chunksFor(ctx, "chat_text")).toEqual(["two"]);
});
it("never drops chunks the sidecar produces itself", async () => {
const { broadcastChunk } = await import("./context");
const ctx = await createStreamingContext(
"session-1",
new Set(["session-1"]),
);
broadcastChunk(ctx, "session-1", "chat_queued_prompt_start", "{}");
expect(chunksFor(ctx, "chat_queued_prompt_start")).toEqual(["{}"]);
});
it("stamps chunks with a stable per-process boot id", async () => {
const { createSidecarContext, handleHubLiveEvent } = await import(
"./context"
);
const first = await createStreamingContext("session-1");
handleHubLiveEvent(first, {
event: "assistant.delta",
sessionId: "session-1",
payload: { text: "a" },
});
handleHubLiveEvent(first, {
event: "assistant.delta",
sessionId: "session-1",
payload: { text: "b" },
});
const boots = readEvents(first)
.filter((message) => message.event.name === "chat_event")
.map((message) => (message.event.payload as { boot?: string }).boot);
expect(boots).toHaveLength(2);
expect(boots[0]).toBeTruthy();
expect(boots[1]).toBe(boots[0]);
// A replacement sidecar restarts `index` at 1, so it must be
// distinguishable by boot id.
const second = createSidecarContext("/workspace/project");
expect(second.bootId).not.toBe(first.bootId);
release?.();
await disposing;
expect(disposed).toBe(true);
});
});
+380 -168
View File
@@ -36,9 +36,11 @@ import type {
PendingAskQuestion,
PendingToolApproval,
PromptInQueue,
SessionRuntimeBinding,
SidecarContext,
SidecarWebSocketClient,
} from "./types";
import { LOCAL_ENVIRONMENT_ID } from "./types";
const ASK_QUESTION_TIMEOUT_MS = 5 * 60_000;
const hubClientInitialization = new WeakMap<
@@ -117,7 +119,10 @@ export function syncSidecarApprovalReadiness(
const update = previous
.catch(() => undefined)
.then(async () => {
const hubClient = ctx.hubClient;
// The approval capability rides on the shared local hub observer; the
// multi-environment refactor keeps that client on the local binding.
const hubClient =
ctx.runtimeBindings.get(LOCAL_ENVIRONMENT_ID)?.hubClient;
if (!hubClient) return;
await hubClient.updateCapabilities(
[...ctx.wsClients].some(
@@ -187,7 +192,6 @@ function emitChunk(
chunk,
ts,
index: nextIndex,
boot: ctx.bootId,
});
}
@@ -244,7 +248,7 @@ export function serializeQueuedPromptStart(input: {
});
}
function sendPromptsInQueueSnapshot(
export function sendPromptsInQueueSnapshot(
ctx: SidecarContext,
sessionId: string,
): void {
@@ -345,7 +349,6 @@ function handleAgentEvent(
message: event.message,
noticeType: event.noticeType,
reason: event.reason,
metadata: event.metadata,
}),
);
break;
@@ -369,7 +372,6 @@ function handleAgentEvent(
break;
}
case "done": {
cancelSidecarMistakeQuestions(ctx, sessionId, "Run ended");
const session = ctx.liveSessions.get(sessionId);
if (session) {
session.busy = false;
@@ -404,19 +406,7 @@ function handleAgentEvent(
);
break;
}
case "iteration_start": {
const session = ctx.liveSessions.get(sessionId);
if (session) {
// Iterations restart at one for each user run. Keep the previous
// answer only within the run in which it was supplied.
if (event.iteration === 1 || !session.mistakeRecovery) {
session.mistakeRecovery = { latestIteration: event.iteration };
} else {
session.mistakeRecovery.latestIteration = event.iteration;
}
}
break;
}
case "iteration_start":
case "iteration_end":
break;
}
@@ -426,8 +416,10 @@ function handleAgentEvent(
// CoreSessionEvent routing
// ---------------------------------------------------------------------------
// Dedupe by prompt id so a repeated pending_prompt_submitted for the same
// prompt cannot render the user message twice.
// The runtime's queue drain emits a pending_prompts snapshot (head removed)
// and a pending_prompt_submitted event for the same prompt back-to-back, and
// both are translated here into chat_queued_prompt_start — dedupe by prompt
// id or the UI renders the user message twice.
function emitQueuedPromptStart(
ctx: SidecarContext,
sessionId: string,
@@ -488,10 +480,20 @@ export function handleCoreSessionEvent(
session,
mapped.map((item) => item.id),
);
// A shrinking snapshot is not evidence that the head started
// running: the user may have deleted it or the queue may have been
// discarded. Only pending_prompt_submitted announces a start.
const previous = session.promptsInQueue;
session.promptsInQueue = mapped;
if (
previous.length > mapped.length &&
previous[0] &&
previous[0].id !== mapped[0]?.id
) {
emitQueuedPromptStart(ctx, sessionId, session, {
promptId: previous[0].id,
prompt: previous[0].prompt,
attachmentCount: previous[0].attachmentCount ?? 0,
userImages: previous[0].userImages,
});
}
}
sendPromptsInQueueSnapshot(ctx, sessionId);
break;
@@ -523,7 +525,6 @@ export function handleCoreSessionEvent(
}
case "ended": {
const { sessionId, reason } = event.payload;
cancelSidecarMistakeQuestions(ctx, sessionId, "Session ended");
const session = ctx.liveSessions.get(sessionId);
if (session) {
session.busy = false;
@@ -574,24 +575,24 @@ export function createSidecarContext(
observability: {
logger?: BasicLogger;
telemetry?: ITelemetryService;
telemetryUser?: SidecarContext["telemetryUser"];
} = {},
): SidecarContext {
return {
liveSessions: new Map(),
restoringWorkspacePaths: new Set(),
streamIndices: new Map(),
bootId: randomUUID(),
wsClients: new Set(),
pendingApprovals: new Map(),
pendingQuestions: new Map(),
sessionManager: null,
hubClient: null,
workspaceRoot,
runtimeBindings: new Map(),
sessionEnvironmentIds: new Map(),
activeEnvironmentId: LOCAL_ENVIRONMENT_ID,
remoteEnvironments: null,
localWorkspaceRoot: workspaceRoot,
logger: observability.logger,
telemetry: observability.telemetry,
telemetryUser: observability.telemetryUser,
unsubscribeSessionEvents: null,
cloudSessionManager: null,
hubBuildMismatch: null,
};
}
@@ -601,9 +602,7 @@ export async function disposeSidecarContext(
reason = "code_sidecar_shutdown",
): Promise<void> {
const cleanup: Array<Promise<unknown>> = [];
ctx.unsubscribeSessionEvents?.();
ctx.unsubscribeSessionEvents = null;
const approvalCleanup: Array<Promise<unknown>> = [];
for (const [sessionId, session] of ctx.liveSessions) {
discardAllTrackedAttachments(sessionId, session);
@@ -619,7 +618,21 @@ export async function disposeSidecarContext(
}
ctx.wsClients.clear();
for (const pending of ctx.pendingApprovals.values()) {
pending.resolve({ approved: false, reason });
// Cloud sessions outlive this app: denying their approvals on local
// shutdown would fail a tool call on a pod that keeps running and
// could otherwise be answered later (from here or another surface).
// Drop those entries locally and leave the remote approval pending.
if (ctx.cloudSessionManager?.isCloudSession(pending.item.sessionId)) {
continue;
}
try {
approvalCleanup.push(
Promise.resolve(pending.resolve({ approved: false, reason })),
);
} catch (error) {
// Keep disposing the remaining resources, then preserve the failure.
approvalCleanup.push(Promise.reject(error));
}
}
ctx.pendingApprovals.clear();
for (const pending of ctx.pendingQuestions.values()) {
@@ -627,24 +640,32 @@ export async function disposeSidecarContext(
pending.reject(new Error(reason));
}
ctx.pendingQuestions.clear();
// Approval callbacks may need the Hub/cloud clients that are disposed below.
const approvalResults = await Promise.allSettled(approvalCleanup);
const hubClient = ctx.hubClient;
ctx.hubClient = null;
if (hubClient) {
cleanup.push(hubClient.dispose());
const cloudSessionManager = ctx.cloudSessionManager;
ctx.cloudSessionManager = null;
if (cloudSessionManager) {
cleanup.push(cloudSessionManager.dispose());
}
const sessionManager = ctx.sessionManager;
ctx.sessionManager = null;
if (sessionManager) {
cleanup.push(sessionManager.dispose(reason));
for (const binding of ctx.runtimeBindings?.values() ?? []) {
binding.unsubscribeSessionEvents();
cleanup.push(binding.hubClient.dispose());
cleanup.push(binding.sessionManager.dispose(reason));
}
ctx.runtimeBindings?.clear();
ctx.sessionEnvironmentIds?.clear();
if (ctx.remoteEnvironments) {
cleanup.push(ctx.remoteEnvironments.dispose());
ctx.remoteEnvironments = null;
}
// Shuts down the PostHog client the feature flags service owns, flushing
// any pending $feature_flag_called events.
cleanup.push(disposeDesktopFeatureFlagsService());
const results = await Promise.allSettled(cleanup);
const results = [...approvalResults, ...(await Promise.allSettled(cleanup))];
const firstFailure = results.find(
(result): result is PromiseRejectedResult => result.status === "rejected",
);
@@ -731,28 +752,6 @@ export function resolveSidecarAskQuestion(
return true;
}
/** Remove prompts before their session is stopped or replaced in the UI. */
export function cancelSidecarMistakeQuestions(
ctx: SidecarContext,
sessionId: string,
reason: string,
): void {
for (const pending of ctx.pendingQuestions?.values() ?? []) {
if (
pending.item.sessionId !== sessionId ||
pending.item.context?.agentId !== "desktop-mistake-limit"
)
continue;
ctx.pendingQuestions.delete(pending.item.requestId);
if (pending.timeoutId) clearTimeout(pending.timeoutId);
pending.reject(new Error(reason));
sendEvent(ctx, "ask_question_cancelled", {
requestId: pending.item.requestId,
reason,
});
}
}
export function createSidecarRuntimeCapabilities(
ctx: SidecarContext,
): RuntimeCapabilities {
@@ -821,6 +820,7 @@ export function handleHubLiveEvent(
sessionId?: string;
payload?: Record<string, unknown>;
},
options: { relayRawAssistantText?: boolean } = {},
): void {
if (event.event === "approval.requested") {
if (typeof event.payload?.agendaTaskId !== "string") return;
@@ -853,25 +853,27 @@ export function handleHubLiveEvent(
if (!session?.attachedViaHub) {
return;
}
// The observer client and ClineCore's own hub client are separate sockets
// that both receive this session's events. This projection only exists for
// sessions ClineCore is not subscribed to (it subscribes as a side effect
// of start/send/pending_prompts and unsubscribes on stop); once it is,
// `handleCoreSessionEvent` carries everything below and a second copy here
// would double every delta, tool row, and status change.
if (ctx.sessionManager?.hasSessionSubscription(sessionId)) {
return;
}
switch (event.event) {
case "assistant.delta": {
const text =
typeof event.payload?.text === "string" ? event.payload.text : "";
if (text) {
emitChunk(ctx, sessionId, "chat_text", text);
if (options.relayRawAssistantText) {
const text =
typeof event.payload?.text === "string" ? event.payload.text : "";
if (text) emitChunk(ctx, sessionId, "chat_text", text);
}
return;
}
case "assistant.image":
case "assistant.video":
case "assistant.audio":
case "reasoning.delta":
case "tool.started":
case "tool.updated":
case "tool.finished":
// HubRuntimeHost already projects these into the canonical Core event
// stream consumed by handleCoreSessionEvent. Relaying the raw Hub copy
// too duplicates assistant output and tool activity.
return;
case "assistant.media": {
const media = event.payload?.media;
if (isGeneratedMedia(media)) {
@@ -879,83 +881,95 @@ export function handleHubLiveEvent(
}
return;
}
case "reasoning.delta": {
const text =
typeof event.payload?.text === "string" ? event.payload.text : "";
const redacted = event.payload?.redacted === true;
if (!text && !redacted) {
case "usage.updated": {
const delta =
event.payload?.delta &&
typeof event.payload.delta === "object" &&
!Array.isArray(event.payload.delta)
? (event.payload.delta as Record<string, unknown>)
: {};
const totals =
event.payload?.totals &&
typeof event.payload.totals === "object" &&
!Array.isArray(event.payload.totals)
? (event.payload.totals as Record<string, unknown>)
: {};
emitChunk(
ctx,
sessionId,
"chat_usage",
JSON.stringify({
inputTokens: delta.inputTokens,
outputTokens: delta.outputTokens,
cacheReadTokens: delta.cacheReadTokens,
cacheWriteTokens: delta.cacheWriteTokens,
cost: delta.totalCost,
totalInputTokens: totals.inputTokens,
totalOutputTokens: totals.outputTokens,
totalCost: totals.totalCost,
}),
);
return;
}
case "session.pending_prompts": {
const items = Array.isArray(event.payload?.prompts)
? (event.payload.prompts as Array<Record<string, unknown>>)
: [];
const mapped: PromptInQueue[] = items
.map((item) => ({
id: typeof item.id === "string" ? item.id : "",
prompt: typeof item.prompt === "string" ? item.prompt : "",
steer: item.delivery === "steer",
attachmentCount:
typeof item.attachmentCount === "number" ? item.attachmentCount : 0,
userImages: Array.isArray(item.userImages)
? (item.userImages as string[])
: undefined,
}))
.filter((item) => item.id && (item.prompt || item.attachmentCount > 0));
reconcileQueuedAttachments(
session,
mapped.map((item) => item.id),
);
// No "head submitted" inference here, unlike the local queue-drain
// handler: the hub emits an explicit session.pending_prompt_submitted
// for real submissions, and a snapshot can also shrink because a
// prompt was REMOVED — inferring a start would render the deleted
// prompt in the transcript as if it had been sent.
session.promptsInQueue = mapped;
sendPromptsInQueueSnapshot(ctx, sessionId);
return;
}
case "session.pending_prompt_submitted": {
const item =
event.payload?.prompt && typeof event.payload.prompt === "object"
? (event.payload.prompt as Record<string, unknown>)
: undefined;
const promptId = typeof item?.id === "string" ? item.id : "";
if (!promptId) {
return;
}
emitChunk(
ctx,
sessionId,
"chat_reasoning",
JSON.stringify({ text, redacted }),
);
markQueuedAttachmentsSubmitted(session, promptId);
emitQueuedPromptStart(ctx, sessionId, session, {
promptId,
prompt: typeof item?.prompt === "string" ? item.prompt : "",
attachmentCount:
typeof item?.attachmentCount === "number" ? item.attachmentCount : 0,
userImages: Array.isArray(item?.userImages)
? (item.userImages as string[])
: undefined,
});
return;
}
case "tool.started": {
emitChunk(
ctx,
sessionId,
"chat_tool_call_start",
JSON.stringify({
toolCallId:
typeof event.payload?.toolCallId === "string"
? event.payload.toolCallId
: undefined,
toolName:
typeof event.payload?.toolName === "string"
? event.payload.toolName
: "tool",
input: event.payload?.input,
}),
);
case "run.started": {
const statusChanged = session.status !== "running";
session.status = "running";
session.busy = true;
if (statusChanged) {
sendEvent(ctx, "chat_session_status", { sessionId, status: "running" });
}
return;
}
case "tool.updated": {
emitChunk(
ctx,
sessionId,
"chat_tool_call_update",
JSON.stringify({
toolCallId:
typeof event.payload?.toolCallId === "string"
? event.payload.toolCallId
: undefined,
toolName:
typeof event.payload?.toolName === "string"
? event.payload.toolName
: "tool",
update: event.payload?.update,
}),
);
return;
}
case "tool.finished": {
emitChunk(
ctx,
sessionId,
"chat_tool_call_end",
JSON.stringify({
toolCallId:
typeof event.payload?.toolCallId === "string"
? event.payload.toolCallId
: undefined,
toolName:
typeof event.payload?.toolName === "string"
? event.payload.toolName
: "tool",
output: event.payload?.output,
error:
typeof event.payload?.error === "string"
? event.payload.error
: undefined,
}),
);
return;
}
case "run.started":
case "session.attached":
case "session.updated": {
const payloadSession =
@@ -964,20 +978,53 @@ export function handleHubLiveEvent(
!Array.isArray(event.payload.session)
? (event.payload.session as Record<string, unknown>)
: undefined;
const status =
const runtimeStatus =
typeof payloadSession?.status === "string"
? payloadSession.status
: event.event === "run.started"
? "running"
: session.status;
: session.status;
// Hub "pending" means the run is blocked on approval or otherwise
// still active. Desktop has no pending status, so expose it as running
// and keep later prompts on the queue path.
const status = runtimeStatus === "pending" ? "running" : runtimeStatus;
// Core's persisted `running` status also means the interactive runtime
// process is resident; it does not prove a model turn is active. Only
// run.started may move an already-idle attached session to running.
// This is especially important for an idle fork created from handoff
// history, which otherwise renders "Thinking" forever after attach.
if (
runtimeStatus === "running" &&
session.status !== "running" &&
session.config.executionTarget !== "cloud"
) {
return;
}
// Pods emit periodic session.updated snapshots; re-broadcasting an
// unchanged status marks the session unread in the sidebar every time.
const statusChanged = session.status !== status;
session.status = status;
session.busy = status === "running";
sendEvent(ctx, "chat_session_status", { sessionId, status });
if (statusChanged) {
sendEvent(ctx, "chat_session_status", { sessionId, status });
}
return;
}
case "run.completed":
case "run.failed":
case "run.aborted": {
// A failed run carries its reason in payload.error — surface it, or
// the user sees a silent no-op (e.g. "Insufficient balance").
const errorMessage =
event.event === "run.failed" && typeof event.payload?.error === "string"
? event.payload.error.trim()
: "";
if (errorMessage) {
emitChunk(
ctx,
sessionId,
"chat_core_log",
JSON.stringify({ level: "error", message: errorMessage }),
);
}
const reason =
typeof event.payload?.reason === "string"
? event.payload.reason
@@ -1049,7 +1096,7 @@ async function handleHubApprovalRequest(
? (event.payload.policy as ToolApprovalRequest["policy"])
: { autoApprove: false },
});
const client = ctx.hubClient;
const client = ctx.runtimeBindings.get(LOCAL_ENVIRONMENT_ID)?.hubClient;
if (!client)
throw new Error("Hub client disconnected before approval response");
await client.command(
@@ -1079,8 +1126,8 @@ export async function initializeSessionManager(
}),
hub: {
strategy: "require-hub",
workspaceRoot: ctx.workspaceRoot,
cwd: ctx.workspaceRoot,
workspaceRoot: ctx.localWorkspaceRoot,
cwd: ctx.localWorkspaceRoot,
clientType: "code-sidecar",
displayName: "Cline Desktop sidecar",
},
@@ -1091,24 +1138,191 @@ export async function initializeSessionManager(
handleCoreSessionEvent(ctx, event);
});
let hubClient: NodeHubClient;
try {
await ensureSharedHubClient(ctx, sessionManager.runtimeAddress);
hubClient = await ensureSharedHubClient(ctx, sessionManager.runtimeAddress);
} catch (error) {
unsubscribe();
await sessionManager.dispose("code_sidecar_hub_initialization_failed");
throw error;
}
ctx.sessionManager = sessionManager;
ctx.unsubscribeSessionEvents = unsubscribe;
ctx.runtimeBindings.set(LOCAL_ENVIRONMENT_ID, {
environmentId: LOCAL_ENVIRONMENT_ID,
kind: "local",
workspaceRoot: ctx.localWorkspaceRoot,
sessionManager,
hubClient,
unsubscribeSessionEvents: unsubscribe,
});
// Advertise the tool-approval surface once the local hub binding exists;
// clients that connected before the hub came up are picked up here.
await syncSidecarApprovalReadiness(ctx).catch((error) =>
ctx.logger?.error?.("Hub approval readiness update failed", { error }),
);
}
export function getRuntimeBinding(
ctx: SidecarContext,
environmentId = ctx.activeEnvironmentId,
): SessionRuntimeBinding {
const binding = ctx.runtimeBindings.get(environmentId);
if (!binding) {
throw new Error(`Environment ${environmentId} is not connected.`);
}
return binding;
}
export function getSessionRuntimeBinding(
ctx: SidecarContext,
sessionId?: string,
requestedEnvironmentId?: string,
): SessionRuntimeBinding {
const environmentId =
requestedEnvironmentId?.trim() ||
(sessionId ? ctx.liveSessions.get(sessionId)?.environmentId : undefined) ||
(sessionId ? ctx.sessionEnvironmentIds.get(sessionId) : undefined) ||
ctx.activeEnvironmentId;
return getRuntimeBinding(ctx, environmentId);
}
export async function findSessionRuntimeBinding(
ctx: SidecarContext,
sessionId: string,
preferredEnvironmentId?: string,
): Promise<SessionRuntimeBinding | undefined> {
const knownEnvironmentId =
preferredEnvironmentId?.trim() ||
ctx.liveSessions.get(sessionId)?.environmentId ||
ctx.sessionEnvironmentIds.get(sessionId);
const candidates = [
...(knownEnvironmentId
? [ctx.runtimeBindings.get(knownEnvironmentId)]
: []),
...ctx.runtimeBindings.values(),
].filter(
(binding, index, all): binding is SessionRuntimeBinding =>
Boolean(binding) && all.indexOf(binding) === index,
);
for (const binding of candidates) {
try {
if (await binding.sessionManager.get(sessionId)) {
ctx.sessionEnvironmentIds.set(sessionId, binding.environmentId);
return binding;
}
} catch {
// A disconnected environment must not prevent another runtime from
// resolving the session.
}
}
return undefined;
}
async function disposeRuntimeBinding(
binding: SessionRuntimeBinding,
reason: string,
): Promise<void> {
try {
binding.unsubscribeSessionEvents();
} catch {
// Continue disposing the Hub clients even if an event source has already
// torn down its subscription.
}
await Promise.allSettled([
binding.hubClient.dispose(),
binding.sessionManager.dispose(reason),
]);
}
export async function connectRemoteSessionRuntime(
ctx: SidecarContext,
connection: NonNullable<SessionRuntimeBinding["remote"]>,
): Promise<SessionRuntimeBinding> {
const environmentId = connection.profile.id;
const existing = ctx.runtimeBindings.get(environmentId);
const sessionManager = await ClineCore.create({
clientName: "cline-code",
backendMode: "remote",
capabilities: createSidecarRuntimeCapabilities(ctx),
logger: ctx.logger,
telemetry: ctx.telemetry,
remote: {
endpoint: connection.endpoint,
authToken: connection.authToken,
workspaceRoot: connection.workspaceRoot,
cwd: connection.workspaceRoot,
clientType: "code-sidecar-ssh",
displayName: `Code App (${connection.profile.name})`,
},
});
let unsubscribe: (() => void) | undefined;
let hubClient: NodeHubClient | undefined;
try {
unsubscribe = sessionManager.subscribe((event: CoreSessionEvent) => {
handleCoreSessionEvent(ctx, event);
});
hubClient = new NodeHubClient({
url: connection.endpoint,
authToken: connection.authToken,
clientType: "code-sidecar-ssh-observer",
displayName: `Code App observer (${connection.profile.name})`,
workspaceRoot: connection.workspaceRoot,
cwd: connection.workspaceRoot,
});
await hubClient.connect();
hubClient.subscribe((event) => handleHubLiveEvent(ctx, event));
} catch (error) {
try {
unsubscribe?.();
} catch {
// Best effort; the failed runtime still needs to be disposed below.
}
const disposals: Promise<unknown>[] = [
sessionManager.dispose("code_sidecar_remote_initialization_failed"),
];
if (hubClient) disposals.push(hubClient.dispose());
await Promise.allSettled(disposals);
throw error;
}
const binding: SessionRuntimeBinding = {
environmentId,
kind: "ssh",
workspaceRoot: connection.workspaceRoot,
sessionManager,
hubClient,
unsubscribeSessionEvents: unsubscribe,
remote: connection,
};
ctx.runtimeBindings.set(environmentId, binding);
ctx.activeEnvironmentId = environmentId;
if (existing) {
await disposeRuntimeBinding(existing, "code_sidecar_remote_reconnect");
}
return binding;
}
export async function disconnectRemoteSessionRuntime(
ctx: SidecarContext,
environmentId: string,
): Promise<void> {
const binding = ctx.runtimeBindings.get(environmentId);
if (binding?.kind === "ssh") {
ctx.runtimeBindings.delete(environmentId);
await disposeRuntimeBinding(binding, "code_sidecar_remote_disconnect");
}
if (ctx.activeEnvironmentId === environmentId) {
ctx.activeEnvironmentId = LOCAL_ENVIRONMENT_ID;
}
}
export async function ensureSharedHubClient(
ctx: SidecarContext,
preferredUrl?: string,
): Promise<NodeHubClient> {
if (ctx.hubClient) {
return ctx.hubClient;
const existing = ctx.runtimeBindings.get(LOCAL_ENVIRONMENT_ID)?.hubClient;
if (existing) {
return existing;
}
const pending = hubClientInitialization.get(ctx);
if (pending) {
@@ -1120,8 +1334,8 @@ export async function ensureSharedHubClient(
preferredUrl?.trim() ||
(await ensureCompatibleLocalHubUrl({
strategy: "require-hub",
workspaceRoot: ctx.workspaceRoot,
cwd: ctx.workspaceRoot,
workspaceRoot: ctx.localWorkspaceRoot,
cwd: ctx.localWorkspaceRoot,
}));
if (!url) {
throw new Error("Unable to start or connect to the shared Cline Hub.");
@@ -1131,16 +1345,14 @@ export async function ensureSharedHubClient(
url,
clientType: "code-sidecar-observer",
displayName: "Cline Desktop observer",
workspaceRoot: ctx.workspaceRoot,
cwd: ctx.workspaceRoot,
workspaceRoot: ctx.localWorkspaceRoot,
cwd: ctx.localWorkspaceRoot,
});
try {
await client.connect();
client.subscribe((event) => {
handleHubLiveEvent(ctx, event);
});
ctx.hubClient = client;
await syncSidecarApprovalReadiness(ctx);
return client;
} catch (error) {
await client.dispose().catch(() => undefined);
@@ -36,18 +36,23 @@ describe("desktop settings", () => {
cloudSessionsEnabled: true,
});
expect(readDesktopSettings()).toEqual({ cloudSessionsEnabled: true });
expect(resolveDesktopSettingsPath().endsWith("code-settings.json")).toBe(
true,
);
expect(
JSON.parse(readFileSync(resolveDesktopSettingsPath(), "utf8")),
).toMatchObject({ cloudSessionsEnabled: true });
expect(setCloudSessionsEnabled(false)).toEqual({
cloudSessionsEnabled: false,
});
expect(readDesktopSettings()).toEqual({ cloudSessionsEnabled: false });
});
it("writes into the desktop-owned settings file, not global-settings", () => {
setCloudSessionsEnabled(true);
const raw = JSON.parse(
readFileSync(resolveDesktopSettingsPath(), "utf8"),
) as Record<string, unknown>;
expect(resolveDesktopSettingsPath().endsWith("code-settings.json")).toBe(
true,
);
expect(raw.cloudSessionsEnabled).toBe(true);
});
it("treats malformed files and non-boolean values as off", () => {
mkdirSync(dirname(resolveDesktopSettingsPath()), { recursive: true });
writeFileSync(resolveDesktopSettingsPath(), "{not json", "utf8");
@@ -2,7 +2,13 @@ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { resolveClineDataDir } from "@cline/shared/storage";
/** Desktop-only preferences kept separate from strict shared global settings. */
/**
* Desktop-app-only preferences.
*
* These are kept out of the shared `global-settings.json` on purpose: that
* file is parsed with a strict schema by every Cline app, and an older CLI
* writing settings would silently strip fields it does not know about.
*/
export type DesktopSettings = {
/** Opt-in gate for cloud sessions while the feature is in preview. */
cloudSessionsEnabled: boolean;
@@ -36,7 +42,8 @@ export function readDesktopSettings(): DesktopSettings {
export function writeDesktopSettings(settings: DesktopSettings): void {
const filePath = resolveDesktopSettingsPath();
mkdirSync(dirname(filePath), { recursive: true });
// Avoid leaving torn settings if the process exits mid-write.
// Write-then-rename keeps the file whole if two app instances race or the
// process dies mid-write; a torn JSON file would silently reset settings.
const tempPath = `${filePath}.${process.pid}.tmp`;
writeFileSync(tempPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
renameSync(tempPath, filePath);
@@ -1,4 +1,4 @@
import { mkdtempSync, rmSync } from "node:fs";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -21,8 +21,8 @@ const mocks = vi.hoisted(() => ({
poll: vi.fn(async () => {}),
dispose: vi.fn(async () => {}),
setContext: vi.fn(),
getBooleanFlagEnabled: vi.fn((_flag: unknown): boolean => false),
getFlagPayload: vi.fn((_flag: unknown): unknown => undefined),
getBooleanFlagEnabled: vi.fn((_flag: unknown): boolean => false),
}));
vi.mock("@cline/core", async () => {
@@ -43,8 +43,8 @@ vi.mock("@cline/core", async () => {
poll = mocks.poll;
dispose = mocks.dispose;
setContext = mocks.setContext;
getBooleanFlagEnabled = mocks.getBooleanFlagEnabled;
getFlagPayload = mocks.getFlagPayload;
getBooleanFlagEnabled = mocks.getBooleanFlagEnabled;
},
};
});
@@ -54,11 +54,15 @@ vi.mock("@cline/core/services/feature-flags/posthog", () => ({
PostHogFeatureFlagsProvider: mocks.PostHogFeatureFlagsProvider,
}));
import { InternalFeature } from "@cline/shared";
import { setCloudSessionsEnabled } from "./desktop-settings";
import {
buildFeatureFlagsSnapshot,
disposeDesktopFeatureFlagsService,
getDesktopFeatureFlagsContext,
getDesktopFeatureFlagsService,
isCloudAgentsEnabled,
isDesktopInternalFeatureEnabled,
refreshDesktopFeatureFlags,
resetDesktopFeatureFlagsForTesting,
setDesktopFeatureFlagsAccountContext,
@@ -66,26 +70,25 @@ import {
const originalApiKey = process.env.TELEMETRY_SERVICE_API_KEY;
const originalIsTest = process.env.IS_TEST;
const originalDataDir = process.env.CLINE_DATA_DIR;
let dataDir: string;
function accountContextPath(): string {
return join(dataDir, "cache", "feature-flags-account.cline-code.json");
}
beforeEach(() => {
dataDir = mkdtempSync(join(tmpdir(), "cline-feature-flags-"));
process.env.CLINE_DATA_DIR = dataDir;
vi.clearAllMocks();
mocks.getBooleanFlagEnabled.mockReset().mockReturnValue(false);
resetDesktopFeatureFlagsForTesting();
delete process.env.IS_TEST;
delete process.env.E2E_TEST;
});
afterEach(() => {
delete process.env.CLINE_CODE_CLOUD_AGENTS;
delete process.env.CLINE_DATA_DIR;
rmSync(dataDir, { recursive: true, force: true });
if (originalDataDir === undefined) {
delete process.env.CLINE_DATA_DIR;
} else {
process.env.CLINE_DATA_DIR = originalDataDir;
}
if (originalApiKey === undefined) {
delete process.env.TELEMETRY_SERVICE_API_KEY;
} else {
@@ -187,6 +190,126 @@ describe("feature flags context", () => {
expect(context.userId).toBe("acct-2");
expect(context.distinctId).toBe("acct-2");
});
it("keeps the known email when an ID-only sync re-confirms the same account", () => {
setDesktopFeatureFlagsAccountContext({
id: "acct-1",
email: "beatrix@cline.bot",
});
// e.g. syncFeatureFlagsAccountFromSettings only knows the account ID.
expect(setDesktopFeatureFlagsAccountContext({ id: "acct-1" })).toBe(false);
expect(getDesktopFeatureFlagsContext().email).toBe("beatrix@cline.bot");
});
it("drops the email when the account changes or signs out", () => {
setDesktopFeatureFlagsAccountContext({
id: "acct-1",
email: "beatrix@cline.bot",
});
setDesktopFeatureFlagsAccountContext({ id: "acct-2" });
expect(getDesktopFeatureFlagsContext().email).toBeUndefined();
setDesktopFeatureFlagsAccountContext({
id: "acct-2",
email: "other@cline.bot",
});
setDesktopFeatureFlagsAccountContext({});
expect(getDesktopFeatureFlagsContext().email).toBeUndefined();
});
});
describe("account context persistence", () => {
it("remembers the account identity across a sidecar restart", () => {
setDesktopFeatureFlagsAccountContext({
id: "acct-1",
email: "beatrix@cline.bot",
});
expect(existsSync(accountContextPath())).toBe(true);
// Simulate a fresh sidecar process: in-memory state gone, file kept.
resetDesktopFeatureFlagsForTesting();
const context = getDesktopFeatureFlagsContext();
expect(context.userId).toBe("acct-1");
expect(context.distinctId).toBe("acct-1");
expect(context.email).toBe("beatrix@cline.bot");
});
it("deletes the persisted identity on sign-out", () => {
setDesktopFeatureFlagsAccountContext({
id: "acct-1",
email: "beatrix@cline.bot",
});
setDesktopFeatureFlagsAccountContext({});
expect(existsSync(accountContextPath())).toBe(false);
resetDesktopFeatureFlagsForTesting();
expect(getDesktopFeatureFlagsContext().userId).toBeUndefined();
});
it("a fetched account wins over a stale hydrated one", () => {
setDesktopFeatureFlagsAccountContext({
id: "acct-old",
email: "old@cline.bot",
});
resetDesktopFeatureFlagsForTesting();
setDesktopFeatureFlagsAccountContext({
id: "acct-new",
email: "new@example.com",
});
const context = getDesktopFeatureFlagsContext();
expect(context.userId).toBe("acct-new");
expect(context.email).toBe("new@example.com");
});
});
describe("isDesktopInternalFeatureEnabled", () => {
it("grants access to @cline.bot accounts", () => {
setDesktopFeatureFlagsAccountContext({
id: "acct-1",
email: "beatrix@cline.bot",
});
expect(
isDesktopInternalFeatureEnabled(InternalFeature.COMPOSIO_CONNECTORS),
).toBe(true);
});
it("fails closed for external and signed-out accounts", () => {
expect(
isDesktopInternalFeatureEnabled(InternalFeature.COMPOSIO_CONNECTORS),
).toBe(false);
setDesktopFeatureFlagsAccountContext({
id: "acct-1",
email: "user@example.com",
});
expect(
isDesktopInternalFeatureEnabled(InternalFeature.COMPOSIO_CONNECTORS),
).toBe(false);
});
it("grants access to external accounts via the feature flag", () => {
setDesktopFeatureFlagsAccountContext({
id: "acct-1",
email: "user@example.com",
});
mocks.getBooleanFlagEnabled.mockImplementation(
(flag: unknown) => flag === InternalFeature.COMPOSIO_CONNECTORS,
);
expect(
isDesktopInternalFeatureEnabled(InternalFeature.COMPOSIO_CONNECTORS),
).toBe(true);
});
it("survives a restart via the persisted account identity", () => {
setDesktopFeatureFlagsAccountContext({
id: "acct-1",
email: "beatrix@cline.bot",
});
resetDesktopFeatureFlagsForTesting();
expect(
isDesktopInternalFeatureEnabled(InternalFeature.COMPOSIO_CONNECTORS),
).toBe(true);
});
});
describe("buildFeatureFlagsSnapshot", () => {
@@ -256,52 +379,27 @@ describe("disposeDesktopFeatureFlagsService", () => {
});
});
describe("cloud agents gate", () => {
beforeEach(() => {
delete process.env.CLINE_CODE_CLOUD_AGENTS;
});
it("is unavailable and disabled while the rollout flag is off", async () => {
const { isCloudAgentsAvailable, isCloudAgentsEnabled } = await import(
"./feature-flags"
);
expect(isCloudAgentsAvailable()).toBe(false);
describe("isCloudAgentsEnabled", () => {
it("defaults to off with no setting and no override", () => {
expect(isCloudAgentsEnabled()).toBe(false);
});
it("does not enable a boolean rollout from a truthy variant payload", async () => {
const { isCloudAgentsAvailable } = await import("./feature-flags");
mocks.getFlagPayload.mockReturnValue("control");
expect(isCloudAgentsAvailable()).toBe(false);
expect(mocks.getBooleanFlagEnabled).toHaveBeenCalledWith(
"code-cloud-agents",
);
});
it("needs both the rollout flag and the user's opt-in to enable", async () => {
const { isCloudAgentsEnabled, isCloudAgentsAvailable } = await import(
"./feature-flags"
);
const { setCloudSessionsEnabled } = await import("./desktop-settings");
mocks.getBooleanFlagEnabled.mockImplementation(
(flag: unknown) => flag === "code-cloud-agents",
);
expect(isCloudAgentsAvailable()).toBe(true);
setCloudSessionsEnabled(false);
expect(isCloudAgentsEnabled()).toBe(false);
it("follows the user's settings opt-in toggle", () => {
setCloudSessionsEnabled(true);
expect(isCloudAgentsEnabled()).toBe(true);
setCloudSessionsEnabled(false);
expect(isCloudAgentsEnabled()).toBe(false);
});
it("lets the env override force the gate in both directions", async () => {
const { isCloudAgentsEnabled, isCloudAgentsAvailable } = await import(
"./feature-flags"
);
mocks.getFlagPayload.mockReturnValue(undefined);
it("honors the env override in both directions", () => {
process.env.CLINE_CODE_CLOUD_AGENTS = "1";
expect(isCloudAgentsAvailable()).toBe(true);
expect(isCloudAgentsEnabled()).toBe(true);
process.env.CLINE_CODE_CLOUD_AGENTS = "true";
expect(isCloudAgentsEnabled()).toBe(true);
setCloudSessionsEnabled(true);
process.env.CLINE_CODE_CLOUD_AGENTS = "0";
expect(isCloudAgentsEnabled()).toBe(false);
process.env.CLINE_CODE_CLOUD_AGENTS = "false";
expect(isCloudAgentsEnabled()).toBe(false);
});
});
@@ -1,11 +1,20 @@
import { join } from "node:path";
import {
existsSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import {
type BasicLogger,
FEATURE_FLAGS,
type FeatureFlagPayload,
type FeatureFlagsContext,
FeatureFlagsService,
type InternalFeature,
type ITelemetryService,
isInternalFeatureEnabled,
NoOpFeatureFlagsProvider,
resolveCoreDistinctId,
} from "@cline/core";
@@ -13,23 +22,99 @@ import {
buildClinePostHogClient,
PostHogFeatureFlagsProvider,
} from "@cline/core/services/feature-flags/posthog";
import { FeatureFlag as SharedFeatureFlag } from "@cline/shared";
import { resolveClineDataDir } from "@cline/shared/storage";
import { readDesktopSettings } from "./desktop-settings";
const FEATURE_FLAG_CODE_CLOUD_AGENTS = SharedFeatureFlag.CODE_CLOUD_AGENTS;
const DESKTOP_FEATURE_FLAGS_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
const DESKTOP_ACCOUNT_CONTEXT_FILE_VERSION = 1;
let desktopFeatureFlagsContext: FeatureFlagsContext = {
clientName: "cline-code",
};
let desktopFeatureFlagsService: FeatureFlagsService | undefined;
let desktopAccountContextHydrated = false;
function resolveDesktopFeatureFlagsCachePath(): string {
return join(resolveClineDataDir(), "cache", "feature-flags.cline-code.json");
}
/**
* Where the last-known account identity ({@link setDesktopFeatureFlagsAccountContext})
* is remembered between launches. The account email only reaches the sidecar
* when the webview fetches the account; without this file, internal-feature
* gates would open only after that fetch on every launch.
*/
function resolveDesktopAccountContextPath(): string {
return join(
resolveClineDataDir(),
"cache",
"feature-flags-account.cline-code.json",
);
}
function hydrateDesktopAccountContextOnce(): void {
if (desktopAccountContextHydrated) {
return;
}
desktopAccountContextHydrated = true;
try {
const path = resolveDesktopAccountContextPath();
if (!existsSync(path)) {
return;
}
const parsed = JSON.parse(readFileSync(path, "utf8")) as {
version?: unknown;
userId?: unknown;
email?: unknown;
};
if (
parsed?.version !== DESKTOP_ACCOUNT_CONTEXT_FILE_VERSION ||
typeof parsed.userId !== "string" ||
!parsed.userId
) {
return;
}
desktopFeatureFlagsContext = {
...desktopFeatureFlagsContext,
distinctId: parsed.userId,
userId: parsed.userId,
email:
typeof parsed.email === "string" && parsed.email
? parsed.email
: undefined,
};
} catch {
// A missing or corrupt file only delays gating until the account fetch.
}
}
function persistDesktopAccountContext(logger?: BasicLogger): void {
try {
const path = resolveDesktopAccountContextPath();
const { userId, email } = desktopFeatureFlagsContext;
if (!userId) {
rmSync(path, { force: true });
return;
}
mkdirSync(dirname(path), { recursive: true });
writeFileSync(
path,
`${JSON.stringify(
{
version: DESKTOP_ACCOUNT_CONTEXT_FILE_VERSION,
userId,
email: email ?? undefined,
},
null,
2,
)}\n`,
{ mode: 0o600 },
);
} catch (error) {
logger?.error?.("Error persisting desktop account context", { error });
}
}
function ensureDesktopDistinctId(): string {
const distinctId = desktopFeatureFlagsContext.distinctId?.trim();
if (distinctId) {
@@ -41,10 +126,30 @@ function ensureDesktopDistinctId(): string {
}
export function getDesktopFeatureFlagsContext(): FeatureFlagsContext {
hydrateDesktopAccountContextOnce();
ensureDesktopDistinctId();
return { ...desktopFeatureFlagsContext };
}
/**
* Whether this install may use an internal-only feature: the signed-in
* account has an internal (`@cline.bot`) email learned from the account
* fetch and remembered across launches or the feature's own flag was
* enabled for the account (the PostHog escape hatch). Signed-out and
* never-fetched accounts fail closed.
*/
export function isDesktopInternalFeatureEnabled(
feature: InternalFeature,
options?: { logger?: BasicLogger; telemetry?: ITelemetryService },
): boolean {
hydrateDesktopAccountContextOnce();
return isInternalFeatureEnabled(feature, {
email: desktopFeatureFlagsContext.email,
isFlagEnabled: (flagKey) =>
getDesktopFeatureFlagsService(options).getBooleanFlagEnabled(flagKey),
});
}
export function getDesktopFeatureFlagsService(options?: {
logger?: BasicLogger;
telemetry?: ITelemetryService;
@@ -86,13 +191,25 @@ export async function disposeDesktopFeatureFlagsService(): Promise<void> {
await current.dispose();
}
export function setDesktopFeatureFlagsAccountContext(account: {
id?: string;
email?: string;
}): boolean {
const accountId = account.id?.trim();
export function setDesktopFeatureFlagsAccountContext(
account: {
id?: string;
email?: string;
},
options?: { logger?: BasicLogger },
): boolean {
hydrateDesktopAccountContextOnce();
const accountId = account.id?.trim() || undefined;
const previousUserId = desktopFeatureFlagsContext.userId ?? undefined;
if (previousUserId === (accountId || undefined)) {
const previousEmail = desktopFeatureFlagsContext.email ?? undefined;
// Callers that only know the account ID (e.g. provider-settings syncs)
// must not erase an email a full account fetch already provided — the
// email is what internal-feature gating keys on. A different account (or
// sign-out) always drops it.
const email =
account.email?.trim() ||
(accountId && accountId === previousUserId ? previousEmail : undefined);
if (previousUserId === accountId && previousEmail === email) {
return false;
}
@@ -101,18 +218,21 @@ export function setDesktopFeatureFlagsAccountContext(account: {
...desktopFeatureFlagsContext,
distinctId: accountId,
userId: accountId,
email,
};
} else {
// Drop both identifiers; ensureDesktopDistinctId re-resolves the device
// Drop the identifiers; ensureDesktopDistinctId re-resolves the device
// ID on the next read rather than leaving the old account's ID behind.
const {
distinctId: _distinctId,
userId: _userId,
email: _email,
...rest
} = desktopFeatureFlagsContext;
desktopFeatureFlagsContext = rest;
}
persistDesktopAccountContext(options?.logger);
desktopFeatureFlagsService?.setContext(getDesktopFeatureFlagsContext());
return true;
}
@@ -156,7 +276,7 @@ export async function identifyDesktopFeatureFlagsAccount(
options?: { logger?: BasicLogger; telemetry?: ITelemetryService },
): Promise<void> {
if (
!setDesktopFeatureFlagsAccountContext(account) ||
!setDesktopFeatureFlagsAccountContext(account, options) ||
!desktopFeatureFlagsService
) {
return;
@@ -169,37 +289,25 @@ export async function identifyDesktopFeatureFlagsAccount(
}
}
/**
* Env override first; otherwise the user's explicit opt-in from Settings.
*
* Cloud sessions are in preview, so the gate is a toggle the user flips in
* Settings General (default off) rather than a remote rollout flag.
*/
export function isCloudAgentsEnabled(): boolean {
const override = process.env.CLINE_CODE_CLOUD_AGENTS?.trim().toLowerCase();
if (override === "1" || override === "true") {
return true;
}
if (override === "0" || override === "false") {
return false;
}
return readDesktopSettings().cloudSessionsEnabled;
}
export function resetDesktopFeatureFlagsForTesting(): void {
desktopFeatureFlagsService = undefined;
desktopFeatureFlagsContext = { clientName: "cline-code" };
}
export function readCloudAgentsEnvOverride(): boolean | undefined {
const override = process.env.CLINE_CODE_CLOUD_AGENTS?.trim().toLowerCase();
if (override === "1" || override === "true") return true;
if (override === "0" || override === "false") return false;
return undefined;
}
/** Whether the rollout makes cloud sessions available to this install. */
export function isCloudAgentsAvailable(options?: {
logger?: BasicLogger;
telemetry?: ITelemetryService;
}): boolean {
const override = readCloudAgentsEnvOverride();
if (override !== undefined) return override;
return getDesktopFeatureFlagsService(options).getBooleanFlagEnabled(
FEATURE_FLAG_CODE_CLOUD_AGENTS,
);
}
/** Whether cloud sessions are both available and enabled by the user. */
export function isCloudAgentsEnabled(options?: {
logger?: BasicLogger;
telemetry?: ITelemetryService;
}): boolean {
const override = readCloudAgentsEnvOverride();
if (override !== undefined) return override;
if (!isCloudAgentsAvailable(options)) return false;
return readDesktopSettings().cloudSessionsEnabled;
desktopAccountContextHydrated = false;
}
+5 -7
View File
@@ -4,10 +4,10 @@ import {
createClineTelemetryServiceConfig,
readGlobalSettings,
setHomeDirIfUnset,
setModelToolEnabledGlobally,
setOptInToolEnabledGlobally,
watchManagedHubBuildMismatch,
} from "@cline/core";
import { captureSdkError, claimHubDaemonProcess } from "@cline/shared";
import { captureSdkError } from "@cline/shared";
import { prewarmWorkspaceMetadata } from "./chat-session";
import { configureConnectorCliLaunch } from "./connectors";
import {
@@ -18,6 +18,7 @@ import {
} from "./context";
import { createDesktopObservability } from "./observability";
import { resolveWorkspaceRoot } from "./paths";
import { runRemoteHelperEntrypoint } from "./remote-helper";
import { startServer } from "./server";
import { ensureLoginShellPath } from "./shell-path";
import { buildTelemetrySelfcheckReport } from "./telemetry-selfcheck";
@@ -74,7 +75,7 @@ async function main() {
// unwritable settings file must not block startup over a default.
try {
if (readGlobalSettings().tools?.web_search === undefined) {
setModelToolEnabledGlobally("web_search", true);
setOptInToolEnabledGlobally("web_search", true);
}
} catch (error) {
observability.logger.error?.("Failed to seed web search default", {
@@ -232,10 +233,7 @@ async function runEntrypoint(): Promise<void> {
runTelemetrySelfcheck();
return;
}
// Claim rather than read: consuming the sentinel keeps daemon-hosted sessions
// from handing it to every process they spawn.
if (claimHubDaemonProcess()) {
await import("@cline/core/hub/daemon-entry");
if (await runRemoteHelperEntrypoint()) {
return;
}
await main();
@@ -1,72 +0,0 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { clearLegacyProviderCredentials } from "./legacy-provider-credentials";
describe("clearLegacyProviderCredentials", () => {
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
it("removes only the Codex credentials from the legacy secrets file", () => {
const dataDir = mkdtempSync(path.join(os.tmpdir(), "desktop-legacy-"));
tempDirs.push(dataDir);
const secretsPath = path.join(dataDir, "secrets.json");
writeFileSync(
secretsPath,
JSON.stringify({
"openai-codex-oauth-credentials": JSON.stringify({
access_token: "a",
refresh_token: "r",
}),
openRouterApiKey: "sk-or-keep",
}),
);
expect(clearLegacyProviderCredentials("openai-codex", dataDir)).toBe(true);
expect(JSON.parse(readFileSync(secretsPath, "utf8"))).toEqual({
openRouterApiKey: "sk-or-keep",
});
});
it("removes both Cline account secrets from the legacy secrets file", () => {
const dataDir = mkdtempSync(path.join(os.tmpdir(), "desktop-legacy-"));
tempDirs.push(dataDir);
const secretsPath = path.join(dataDir, "secrets.json");
writeFileSync(
secretsPath,
JSON.stringify({
"cline:clineAccountId": JSON.stringify({ idToken: "t" }),
clineApiKey: "cline-key",
openRouterApiKey: "sk-or-keep",
}),
);
expect(clearLegacyProviderCredentials("cline", dataDir)).toBe(true);
expect(JSON.parse(readFileSync(secretsPath, "utf8"))).toEqual({
openRouterApiKey: "sk-or-keep",
});
});
it("is a no-op when the file is missing, has no matching credentials, or the provider is unknown", () => {
const dataDir = mkdtempSync(path.join(os.tmpdir(), "desktop-legacy-"));
tempDirs.push(dataDir);
expect(clearLegacyProviderCredentials("openai-codex", dataDir)).toBe(false);
const secretsPath = path.join(dataDir, "secrets.json");
writeFileSync(secretsPath, JSON.stringify({ apiKey: "keep" }));
expect(clearLegacyProviderCredentials("cline", dataDir)).toBe(false);
expect(clearLegacyProviderCredentials("anthropic", dataDir)).toBe(false);
expect(readFileSync(secretsPath, "utf8")).toBe(
JSON.stringify({ apiKey: "keep" }),
);
writeFileSync(secretsPath, "{not json");
expect(clearLegacyProviderCredentials("openai-codex", dataDir)).toBe(false);
});
});
@@ -1,56 +0,0 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { resolveClineDataDir } from "@cline/shared/storage";
/**
* Legacy VS Code extension secrets.json keys that the legacy import in
* ProviderSettingsManager turns back into a providers.json entry.
*/
const LEGACY_SECRET_KEYS_BY_PROVIDER: Record<string, string[]> = {
"openai-codex": ["openai-codex-oauth-credentials"],
cline: ["cline:clineAccountId", "clineApiKey"],
};
/**
* Removes a provider's credentials from the legacy VS Code extension's
* secrets.json. The legacy import in ProviderSettingsManager runs on
* construction and re-adds any provider missing from providers.json, so
* leaving these credentials on disk would sign the user straight back in
* after they sign out in the desktop app. Temporary until the legacy import
* is retired.
*
* A missing or unparseable file is a no-op (the import ignores those too).
* A failed write throws so the sign-out is reported as failed instead of
* succeeding and then being undone by the next import.
*/
export function clearLegacyProviderCredentials(
providerId: string,
dataDir: string = resolveClineDataDir(),
): boolean {
const keys = LEGACY_SECRET_KEYS_BY_PROVIDER[providerId];
const secretsPath = join(dataDir, "secrets.json");
if (!keys || !existsSync(secretsPath)) {
return false;
}
let secrets: unknown;
try {
secrets = JSON.parse(readFileSync(secretsPath, "utf8"));
} catch {
return false;
}
if (!secrets || typeof secrets !== "object" || Array.isArray(secrets)) {
return false;
}
const present = keys.filter((key) => key in secrets);
if (present.length === 0) {
return false;
}
for (const key of present) {
delete (secrets as Record<string, unknown>)[key];
}
writeFileSync(secretsPath, `${JSON.stringify(secrets, null, 2)}\n`, {
encoding: "utf8",
mode: 0o600,
});
return true;
}
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { handleCommand } from "./commands";
import { createSidecarContext } from "./context";
import {
buildMcpServersResponse,
shouldProbeMcpServerAfterUpsert,
@@ -14,7 +15,6 @@ function createContext(workspaceRoot: string): SidecarContext {
liveSessions: new Map(),
restoringWorkspacePaths: new Set(),
streamIndices: new Map(),
bootId: "test-boot",
wsClients: new Set(),
pendingApprovals: new Map(),
pendingQuestions: new Map(),
@@ -22,8 +22,9 @@ function createContext(workspaceRoot: string): SidecarContext {
hubClient: null,
workspaceRoot,
unsubscribeSessionEvents: null,
cloudSessionManager: null,
hubBuildMismatch: null,
};
} as unknown as SidecarContext;
}
describe("desktop MCP settings", () => {
@@ -1,13 +1,10 @@
import type { ProviderSettingsManager } from "@cline/core";
import {
completeClineDeviceAuth,
getProviderAuthStorageId,
loginLocalProvider,
markLocalProviderEnabled,
saveLocalProviderOAuthCredentials,
startClineDeviceAuth,
} from "@cline/core";
import { getClineEnvironmentConfig } from "@cline/shared";
export class OAuthLoginCancelledError extends Error {
constructor(providerId: string) {
@@ -29,41 +26,13 @@ type PendingOAuthLogin = {
const pendingOAuthLoginsByProvider = new Map<string, PendingOAuthLogin>();
export type OAuthLoginDependencies = {
login: typeof loginProviderForDesktop;
login: typeof loginLocalProvider;
save: typeof saveLocalProviderOAuthCredentials;
markEnabled: typeof markLocalProviderEnabled;
};
/**
* Cline account providers sign in with the WorkOS device-code grant, whose
* browser page asks the user to confirm a short code. `loginLocalProvider`
* runs that flow but discards the code, so use the split helpers instead and
* surface the code through `onUserCode` for the UI to display.
*/
async function loginProviderForDesktop(
providerId: string,
existing: Parameters<typeof loginLocalProvider>[1],
openUrl: (url: string) => void,
onUserCode?: (userCode: string) => void,
): ReturnType<typeof loginLocalProvider> {
if (providerId !== "cline" && providerId !== "cline-pass") {
return loginLocalProvider(providerId, existing, openUrl);
}
const device = await startClineDeviceAuth();
onUserCode?.(device.userCode);
openUrl(device.verificationUriComplete ?? device.verificationUri);
return completeClineDeviceAuth({
deviceCode: device.deviceCode,
expiresInSeconds: device.expiresInSeconds,
pollIntervalSeconds: device.pollIntervalSeconds,
apiBaseUrl:
existing?.baseUrl?.trim() || getClineEnvironmentConfig().apiBaseUrl,
provider: providerId,
});
}
const defaultDependencies: OAuthLoginDependencies = {
login: loginProviderForDesktop,
login: loginLocalProvider,
save: saveLocalProviderOAuthCredentials,
markEnabled: markLocalProviderEnabled,
};
@@ -78,11 +47,7 @@ export async function runCancellableProviderOAuthLogin(
manager: ProviderSettingsManager,
providerId: string,
openUrl: (url: string) => void,
options: {
owner?: object;
/** Receives the device sign-in confirmation code, when the flow has one. */
onUserCode?: (userCode: string) => void;
} = {},
options: { owner?: object } = {},
dependencies: OAuthLoginDependencies = defaultDependencies,
): Promise<{ provider: string; accessToken: string }> {
const storageProviderId = getProviderAuthStorageId(providerId) ?? providerId;
@@ -109,7 +74,7 @@ export async function runCancellableProviderOAuthLogin(
// after cancellation is observed and cannot become an unhandled
// rejection that kills the sidecar.
const credentials = await Promise.race([
dependencies.login(providerId, existing, openUrl, options.onUserCode),
dependencies.login(providerId, existing, openUrl),
cancellation,
]);
if (entry.cancelled) {
@@ -1,5 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { version } from "../package.json";
const mocks = vi.hoisted(() => ({
captureExtensionActivated: vi.fn(),
@@ -29,14 +28,7 @@ vi.mock("@cline/core", async () => {
identifyAccount: mocks.identifyAccount,
ProviderSettingsManager: class {
getProviderSettings() {
return {
auth: {
accountId: "account-1",
organizationId: "org-1",
organizationName: "Acme",
memberId: "member-1",
},
};
return { auth: { accountId: "account-1" } };
}
},
setSdkLogger: mocks.setSdkLogger,
@@ -65,10 +57,8 @@ describe("desktop observability", () => {
expect(mocks.createClineTelemetryServiceConfig).toHaveBeenCalledWith({
metadata: expect.objectContaining({
extension_version: version,
cline_type: "desktop",
platform: "Cline Desktop",
platform_version: version,
platform: "Cline",
}),
});
expect(mocks.createConfiguredTelemetryHandle).toHaveBeenCalledWith(
@@ -77,18 +67,9 @@ describe("desktop observability", () => {
expect(mocks.identifyAccount).toHaveBeenCalledWith(telemetry, {
id: "account-1",
provider: "cline",
organizationId: "org-1",
organizationName: "Acme",
memberId: "member-1",
});
expect(mocks.captureExtensionActivated).toHaveBeenCalledWith(telemetry);
expect(mocks.setSdkLogger).toHaveBeenCalledWith(logger);
expect(observability.telemetryUser).toEqual({
distinctId: "account-1",
accountId: "account-1",
email: undefined,
organizationId: "org-1",
});
await observability.dispose();
await observability.dispose();
@@ -1,3 +1,4 @@
import * as os from "node:os";
import {
captureExtensionActivated,
createClineTelemetryServiceConfig,
@@ -7,11 +8,7 @@ import {
ProviderSettingsManager,
setSdkLogger,
} from "@cline/core";
import type { UserContext } from "@cline/shared";
import {
DESKTOP_TELEMETRY_METADATA,
resolveDesktopTelemetryUser,
} from "./client-context";
import { version } from "../package.json";
import { setDesktopFeatureFlagsAccountContext } from "./feature-flags";
import {
createDesktopLoggerAdapter,
@@ -21,7 +18,6 @@ import {
export interface DesktopObservability {
readonly logger: DesktopLoggerAdapter["core"];
readonly telemetry: ITelemetryService;
readonly telemetryUser?: UserContext;
dispose(): Promise<void>;
}
@@ -32,23 +28,23 @@ export function createDesktopObservability(): DesktopObservability {
const telemetryHandle = createConfiguredTelemetryHandle({
...createClineTelemetryServiceConfig({
metadata: DESKTOP_TELEMETRY_METADATA,
metadata: {
extension_version: version,
cline_type: "desktop",
platform: "Cline",
platform_version: process.version,
os_type: os.platform(),
os_version: os.version(),
},
}),
logger,
});
const telemetry = telemetryHandle.telemetry;
const auth = new ProviderSettingsManager().getProviderSettings("cline")?.auth;
const telemetryUser = resolveDesktopTelemetryUser({
accountId: auth?.accountId,
organizationId: auth?.organizationId,
});
if (auth?.accountId) {
identifyAccount(telemetry, {
id: auth.accountId,
provider: "cline",
organizationId: auth.organizationId,
organizationName: auth.organizationName,
memberId: auth.memberId,
});
setDesktopFeatureFlagsAccountContext({ id: auth.accountId });
}
@@ -58,7 +54,6 @@ export function createDesktopObservability(): DesktopObservability {
return {
logger,
telemetry,
telemetryUser,
async dispose() {
if (disposed) return;
disposed = true;
@@ -1,68 +0,0 @@
import type { ITelemetryService } from "@cline/shared";
import { expect, it, vi } from "vitest";
import { capturePullRequestEvent } from "./pull-request-telemetry";
const event = {
action: "open_clicked",
prState: "open",
ciState: "success",
mergeTone: "success",
};
function service(enabled = true) {
const capture = vi.fn();
return {
capture,
telemetry: {
capture,
isEnabled: () => enabled,
} as unknown as ITelemetryService,
};
}
it("captures only allowlisted status categories and strips identifiers", () => {
const { capture, telemetry } = service();
capturePullRequestEvent(telemetry, {
...event,
repository: "private/repo",
cwd: "/private/path",
url: "https://github.com/private/repo",
title: "Secret",
number: 42,
});
expect(capture).toHaveBeenCalledExactlyOnceWith({
event: "desktop.pull_request.open_clicked",
properties: {
prState: "open",
ciState: "success",
mergeTone: "success",
},
});
});
it.each([
{ ...event, action: "arbitrary.event" },
{ ...event, prState: "private/repo" },
{ ...event, ciState: "test name" },
{ ...event, mergeTone: "secret" },
{},
null,
])("drops invalid payloads", (input) => {
const { capture, telemetry } = service();
capturePullRequestEvent(telemetry, input);
expect(capture).not.toHaveBeenCalled();
});
it("respects telemetry opt-out", () => {
const { capture, telemetry } = service(false);
capturePullRequestEvent(telemetry, event);
expect(capture).not.toHaveBeenCalled();
expect(() => capturePullRequestEvent(undefined, event)).not.toThrow();
});
it("does not fail the command when the provider throws", () => {
const { capture, telemetry } = service();
capture.mockImplementation(() => {
throw new Error("Telemetry unavailable");
});
expect(() => capturePullRequestEvent(telemetry, event)).not.toThrow();
});
@@ -1,17 +0,0 @@
import type { ITelemetryService } from "@cline/shared";
import { pullRequestTelemetrySchema } from "../webview/lib/pull-request-telemetry-schema";
export function capturePullRequestEvent(
telemetry: ITelemetryService | undefined,
input: unknown,
): void {
const parsed = pullRequestTelemetrySchema.safeParse(input);
if (!parsed.success) return;
try {
if (!telemetry?.isEnabled()) return;
const { action, ...properties } = parsed.data;
telemetry.capture({ event: `desktop.pull_request.${action}`, properties });
} catch {
// Product interactions must continue if the telemetry provider fails.
}
}
@@ -1,252 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { getMergeStatus, summarizeChecks } from "../webview/lib/pull-request";
import {
createPullRequestStatusReader,
GITHUB_AVAILABILITY_CACHE_MS,
githubRepository,
normalizeCheck,
} from "./pull-request";
const pr = {
number: 42,
title: "Feature",
url: "https://github.com/cline/cline/pull/42",
state: "OPEN",
isDraft: false,
mergeable: "MERGEABLE",
mergeStateStatus: "CLEAN",
additions: 12,
deletions: 3,
headRepositoryOwner: { login: "cline" },
headRepository: { name: "cline" },
statusCheckRollup: [
{
__typename: "CheckRun",
name: "Test",
status: "COMPLETED",
conclusion: "SUCCESS",
},
],
};
function runner(prs: unknown[] = [pr], branch = "feature/pr-ui") {
return vi.fn(async (file: string, args: string[], _cwd: string) => {
if (file === "git")
return args[0] === "branch" ? branch : "git@github.com:cline/cline.git";
if (args[0] === "pr" && args[1] === "view")
return JSON.stringify(
prs.find((item) => (item as typeof pr).number === Number(args[2])),
);
return JSON.stringify(
args[0] === "repo" ? { defaultBranchRef: { name: "main" } } : prs,
);
});
}
describe("pull request status", () => {
it("reads the active workspace, filters same-named fork branches and prefers an open PR", async () => {
const run = runner([
{ ...pr, number: 99, headRepositoryOwner: { login: "someone" } },
{ ...pr, number: 41, state: "MERGED" },
pr,
]);
const result = await createPullRequestStatusReader({ run })("/worktree");
expect(result?.pullRequest?.number).toBe(42);
expect(result?.pullRequest?.checks[0].state).toBe("success");
expect(result?.createUrl).toBe(
"https://github.com/cline/cline/compare/main...feature%2Fpr-ui?expand=1",
);
expect(run.mock.calls.every((call) => call[2] === "/worktree")).toBe(true);
expect(run.mock.calls.find((call) => call[1][0] === "pr")?.[1]).toContain(
"feature/pr-ui",
);
});
it("offers creation for a feature branch and hides it for the default branch", async () => {
expect(
(await createPullRequestStatusReader({ run: runner([]) })("/repo"))
?.pullRequest,
).toBeNull();
expect(
await createPullRequestStatusReader({ run: runner([], "main") })("/repo"),
).toBeNull();
});
it("keeps merged and closed PR states", async () => {
for (const state of ["MERGED", "CLOSED"] as const) {
const result = await createPullRequestStatusReader({
run: runner([{ ...pr, state }]),
})("/repo");
expect(result?.pullRequest?.state).toBe(state);
}
});
it("does not invoke GitHub for detached HEAD or unsupported remotes", async () => {
const detached = runner([], "");
expect(
await createPullRequestStatusReader({ run: detached })("/repo"),
).toBeNull();
expect(detached).toHaveBeenCalledTimes(1);
const local = vi.fn(async () => "local");
expect(
await createPullRequestStatusReader({ run: local })("/repo"),
).toBeNull();
expect(local).toHaveBeenCalledTimes(2);
});
it.each([
"ENOENT",
1,
4,
])("hides unavailable GitHub CLI (%s), shares the cooldown, and recovers after login", async (code) => {
let time = 0;
let authenticated = false;
const base = runner();
const run = vi.fn(async (file: string, args: string[], cwd: string) => {
if (file === "gh" && args[0] === "auth" && !authenticated)
throw Object.assign(new Error("private stderr"), { code });
return base(file, args, cwd);
});
const read = createPullRequestStatusReader({ run, now: () => time });
expect(await read("/repo")).toBeNull();
const attempts = run.mock.calls.length;
authenticated = true;
time = GITHUB_AVAILABILITY_CACHE_MS - 1;
expect(await read("/other-workspace")).toBeNull();
expect(run).toHaveBeenCalledTimes(attempts);
time++;
expect((await read("/repo"))?.pullRequest?.number).toBe(42);
expect(run.mock.calls.filter((call) => call[1][0] === "auth")).toHaveLength(
2,
);
});
it("shares an in-flight availability check across concurrent workspaces", async () => {
let finish!: () => void;
const waiting = new Promise<void>((resolve) => {
finish = resolve;
});
const base = runner();
const run = vi.fn(async (file: string, args: string[], cwd: string) => {
if (args[0] === "auth") await waiting;
return base(file, args, cwd);
});
const read = createPullRequestStatusReader({ run });
const first = read("/first");
const second = read("/second");
await vi.waitFor(() =>
expect(
run.mock.calls.filter((call) => call[1][0] === "auth"),
).toHaveLength(1),
);
finish();
await Promise.all([first, second]);
expect(run.mock.calls.filter((call) => call[1][0] === "auth")).toHaveLength(
1,
);
});
it("hides default-branch authentication failures before any repository query", async () => {
const base = runner([], "main");
const run = vi.fn(async (file: string, args: string[], cwd: string) => {
if (file === "gh")
throw Object.assign(new Error("Not logged in"), { code: 1 });
return base(file, args, cwd);
});
expect(await createPullRequestStatusReader({ run })("/repo")).toBeNull();
expect(
run.mock.calls.filter((call) => call[0] === "gh").map((call) => call[1]),
).toEqual([["auth", "status", "--active", "--hostname", "github.com"]]);
});
it.each([
{ code: 4 },
{ code: 1, stderr: "HTTP 401: Bad credentials" },
])("invalidates cached availability if authentication expires during a lookup", async (failure) => {
let expired = false;
const base = runner();
const run = vi.fn(async (file: string, args: string[], cwd: string) => {
if (args[0] === "repo" && expired)
throw Object.assign(new Error("Failed"), failure);
return base(file, args, cwd);
});
const read = createPullRequestStatusReader({ run });
expect((await read("/repo"))?.pullRequest?.number).toBe(42);
expired = true;
expect(await read("/repo")).toBeNull();
const attempts = run.mock.calls.length;
expect(await read("/other")).toBeNull();
expect(run).toHaveBeenCalledTimes(attempts);
});
it("preserves transient lookup errors after successful authentication", async () => {
const base = runner();
const run = async (file: string, args: string[], cwd: string) => {
if (args[0] === "repo")
throw Object.assign(new Error("private stderr"), {
code: 1,
stderr: "error connecting to api.github.com",
});
return base(file, args, cwd);
};
await expect(
createPullRequestStatusReader({ run })("/repo"),
).rejects.toThrow(
"Could not load pull request status. Check your connection and try again.",
);
});
it("accepts GitHub SSH/HTTPS remotes only", () => {
for (const remote of [
"git@github.com:cline/cline.git",
"https://github.com/cline/cline.git",
"ssh://git@github.com/cline/cline",
])
expect(githubRepository(remote)).toBe("cline/cline");
expect(
githubRepository("https://github.com.evil.test/cline/cline"),
).toBeNull();
});
});
describe("check and merge states", () => {
it("handles check runs, legacy statuses, skipped checks and unsafe links", () => {
const pending = normalizeCheck({
__typename: "CheckRun",
status: "IN_PROGRESS",
conclusion: "SUCCESS",
});
const failed = normalizeCheck({
__typename: "StatusContext",
state: "ERROR",
context: "Build",
targetUrl: "javascript:alert(1)",
});
const skipped = normalizeCheck({
__typename: "CheckRun",
status: "COMPLETED",
conclusion: "SKIPPED",
});
expect(pending.state).toBe("pending");
expect(failed).toEqual({ name: "Build", state: "failure", url: undefined });
expect(summarizeChecks([pending, failed])).toBe("failure");
expect(summarizeChecks([skipped])).toBe("skipped");
expect(summarizeChecks([])).toBe("none");
});
it("never calls an unknown, draft or blocked PR ready to merge", async () => {
const result = await createPullRequestStatusReader({ run: runner() })(
"/repo",
);
const value = result!.pullRequest!;
expect(
getMergeStatus({ ...value, mergeStateStatus: "BLOCKED" }).label,
).toBe("Blocked");
expect(getMergeStatus({ ...value, isDraft: true }).label).toBe("Draft");
expect(
getMergeStatus({
...value,
mergeable: "UNKNOWN",
mergeStateStatus: "UNKNOWN",
}).label,
).toBe("Merge status pending");
expect(getMergeStatus({ ...value, mergeable: "CONFLICTING" }).label).toBe(
"Conflicts",
);
});
});
@@ -1,269 +0,0 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import type {
PullRequestCheck,
PullRequestStatus,
} from "../webview/lib/pull-request";
const execFileAsync = promisify(execFile);
type RunCommand = (
file: string,
args: string[],
cwd: string,
) => Promise<string>;
const runCommand: RunCommand = async (file, args, cwd) => {
const { stdout } = await execFileAsync(file, args, {
cwd,
encoding: "utf8",
timeout: 15_000,
maxBuffer: 2 * 1024 * 1024,
env: { ...process.env, GH_PROMPT_DISABLED: "1", GIT_TERMINAL_PROMPT: "0" },
});
return stdout.trim();
};
type GitHubCheck = {
__typename: string;
name?: string;
context?: string;
status?: string;
conclusion?: string;
state?: string;
detailsUrl?: string;
targetUrl?: string;
};
export function normalizeCheck(check: GitHubCheck): PullRequestCheck {
const result =
check.__typename === "CheckRun"
? check.status === "COMPLETED"
? check.conclusion
: "PENDING"
: check.state;
return {
name: check.name || check.context || "Check",
state:
result === "SUCCESS"
? "success"
: result === "NEUTRAL" || result === "SKIPPED"
? "skipped"
: [
"FAILURE",
"ERROR",
"CANCELLED",
"TIMED_OUT",
"ACTION_REQUIRED",
"STALE",
"STARTUP_FAILURE",
].includes(result ?? "")
? "failure"
: "pending",
url: safeHttpUrl(check.detailsUrl || check.targetUrl),
};
}
function safeHttpUrl(value?: string): string | undefined {
if (!value) return undefined;
try {
const url = new URL(value);
return ["https:", "http:"].includes(url.protocol) ? url.href : undefined;
} catch {
return undefined;
}
}
export function githubRepository(remote: string): string | null {
const match = remote.match(
/^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/]+)\/([^/]+?)\/?$/,
);
return match ? `${match[1]}/${match[2].replace(/\.git$/, "")}` : null;
}
type GitHubPullRequest = Omit<
NonNullable<PullRequestStatus["pullRequest"]>,
"checks"
> & {
headRepositoryOwner: { login: string } | null;
headRepository: { name: string } | null;
statusCheckRollup: GitHubCheck[] | null;
};
// Shared across workspaces: missing credentials are a machine-level capability,
// not a repository failure. Retry after installation/login without polling gh.
export const GITHUB_AVAILABILITY_CACHE_MS = 5 * 60_000;
function commandErrorCode(error: unknown): unknown {
return error && typeof error === "object" && "code" in error
? error.code
: undefined;
}
function isAuthenticationFailure(error: unknown): boolean {
if (commandErrorCode(error) === 4) return true;
const stderr =
error && typeof error === "object" && "stderr" in error
? String(error.stderr)
: "";
return /HTTP 401|Bad credentials/i.test(stderr);
}
export function createPullRequestStatusReader({
run = runCommand,
now = Date.now,
}: {
run?: RunCommand;
now?: () => number;
} = {}) {
let available: boolean | undefined;
let expiresAt = 0;
let probe: Promise<boolean> | undefined;
function markUnavailable() {
available = false;
expiresAt = now() + GITHUB_AVAILABILITY_CACHE_MS;
}
async function isAvailable(cwd: string): Promise<boolean> {
if (available !== undefined && now() < expiresAt) return available;
if (probe) return probe;
probe = (async () => {
try {
await run(
"gh",
["auth", "status", "--active", "--hostname", "github.com"],
cwd,
);
available = true;
expiresAt = now() + GITHUB_AVAILABILITY_CACHE_MS;
return true;
} catch (error) {
// gh auth status documents exit 1 for missing/invalid authentication.
const code = commandErrorCode(error);
if (code === "ENOENT" || code === 1 || code === 4) {
markUnavailable();
return false;
}
throw error;
}
})();
try {
return await probe;
} finally {
probe = undefined;
}
}
/** Read-only: creation is reviewed and submitted in GitHub's compare form. */
return async function readPullRequestStatus(
cwd: string,
): Promise<PullRequestStatus | null> {
if (available === false && now() < expiresAt) return null;
const branch = await run("git", ["branch", "--show-current"], cwd).catch(
() => "",
);
if (!branch) return null;
const remote = await run("git", ["remote", "get-url", "origin"], cwd).catch(
() => "",
);
const repository = githubRepository(remote);
if (!repository) return null;
try {
if (!(await isAvailable(cwd))) return null;
const repo = JSON.parse(
await run(
"gh",
["repo", "view", repository, "--json", "defaultBranchRef"],
cwd,
),
) as { defaultBranchRef: { name: string } | null };
const base = repo.defaultBranchRef?.name;
// The default branch can have years-old PRs from earlier branch workflows.
// Those do not describe the current work, and it is not a PR source branch.
if (branch === base) return null;
const prsJson = await run(
"gh",
[
"pr",
"list",
"--repo",
repository,
"--head",
branch,
"--state",
"all",
"--limit",
"100",
"--json",
"number,state,headRepositoryOwner,headRepository",
],
cwd,
);
const [owner, name] = repository.split("/");
const candidates = (
JSON.parse(prsJson) as Pick<
GitHubPullRequest,
"number" | "state" | "headRepositoryOwner" | "headRepository"
>[]
).filter(
(pr) =>
pr.headRepositoryOwner?.login.toLowerCase() === owner.toLowerCase() &&
pr.headRepository?.name.toLowerCase() === name.toLowerCase(),
);
const candidate =
candidates.find((pr) => pr.state === "OPEN") ?? candidates[0];
const pr = candidate
? (JSON.parse(
await run(
"gh",
[
"pr",
"view",
String(candidate.number),
"--repo",
repository,
"--json",
"number,title,url,state,isDraft,mergeable,mergeStateStatus,additions,deletions,statusCheckRollup",
],
cwd,
),
) as GitHubPullRequest)
: null;
return {
repository,
branch,
createUrl:
base && branch !== base
? `https://github.com/${repository}/compare/${encodeURIComponent(base)}...${encodeURIComponent(branch)}?expand=1`
: null,
pullRequest: pr
? {
number: pr.number,
title: pr.title,
url: pr.url,
state: pr.state,
isDraft: pr.isDraft,
mergeable: pr.mergeable,
mergeStateStatus: pr.mergeStateStatus,
additions: pr.additions,
deletions: pr.deletions,
checks: (pr.statusCheckRollup ?? []).map(normalizeCheck),
}
: null,
};
} catch (error) {
// Authentication can expire while a positive availability result is cached.
if (
commandErrorCode(error) === "ENOENT" ||
isAuthenticationFailure(error)
) {
markUnavailable();
return null;
}
throw new Error(
"Could not load pull request status. Check your connection and try again.",
);
}
};
}
export const getPullRequestStatus = createPullRequestStatusReader();
@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { SidecarContext } from "./types";
const mocks = vi.hoisted(() => ({
handleCommand: vi.fn(),
}));
vi.mock("./commands", () => ({
handleCommand: mocks.handleCommand,
}));
import { createFetchHandler } from "./server";
const server = {
port: 3126,
upgrade: vi.fn(() => true),
};
describe("realtime session endpoint", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns the short-lived realtime setup expected by the AI SDK hook", async () => {
mocks.handleCommand.mockResolvedValue({
kind: "realtime",
providerId: "vercel-ai-gateway",
modelId: "openai/gpt-realtime",
supportsTools: true,
token: "ephemeral-token",
url: "wss://realtime.example.test/session",
expiresAt: 1_785_280_000,
transport: "vercel-ai-gateway",
sessionConfig: { outputModalities: ["audio"] },
});
const ctx = {} as SidecarContext;
const response = await createFetchHandler(ctx, vi.fn())(
new Request("http://127.0.0.1:3126/api/modes/realtime/session", {
method: "POST",
headers: { origin: "tauri://localhost" },
}),
server,
);
expect(mocks.handleCommand).toHaveBeenCalledWith(
ctx,
"create_mode_session",
{ mode: "realtimeVoice" },
);
expect(response?.status).toBe(200);
if (!response) throw new Error("Missing realtime endpoint response");
await expect(response.json()).resolves.toEqual({
token: "ephemeral-token",
url: "wss://realtime.example.test/session",
expiresAt: 1_785_280_000,
tools: [
{
type: "function",
name: "run_cline",
description:
"Send the user's complete request to the active Cline agent. You must call this exactly once for every user utterance. Cline owns conversation history, workspace context, tools, MCP, approvals, and persistence. After the tool returns, speak its response faithfully.",
parameters: {
type: "object",
properties: {
request: {
type: "string",
description:
"The user's complete request, preserving all relevant detail.",
},
},
required: ["request"],
additionalProperties: false,
},
},
],
});
});
it("does not invoke the sidecar command for an untrusted origin", async () => {
const response = await createFetchHandler({} as SidecarContext, vi.fn())(
new Request("http://127.0.0.1:3126/api/modes/realtime/session", {
method: "POST",
headers: { origin: "https://attacker.example" },
}),
server,
);
expect(response?.status).toBe(403);
expect(mocks.handleCommand).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,805 @@
import {
mkdirSync,
mkdtempSync,
realpathSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type {
RemoteEnvironmentConnection,
RemoteEnvironmentProfile,
RemoteEnvironmentService,
RemoteEnvironmentStatus,
} from "./remote-environments";
import type { SessionRuntimeBinding, SidecarContext } from "./types";
const coreCreateMock = vi.hoisted(() => vi.fn());
const hubClientConstructorMock = vi.hoisted(() => vi.fn());
const hubConnectMock = vi.hoisted(() => vi.fn());
const hubSubscribeMock = vi.hoisted(() => vi.fn());
const hubDisposeMock = vi.hoisted(() => vi.fn());
const sessionStoreGetMock = vi.hoisted(() => vi.fn());
const sessionStoreDeleteMock = vi.hoisted(() => vi.fn());
const sessionStoreRunMock = vi.hoisted(() => vi.fn());
vi.mock("@cline/core", async () => {
const actual =
await vi.importActual<typeof import("@cline/core")>("@cline/core");
return {
...actual,
ClineCore: {
create: coreCreateMock,
},
SqliteSessionStore: class {
public get(sessionId: string): unknown {
return sessionStoreGetMock(sessionId);
}
public delete(sessionId: string, cascade?: boolean): boolean {
return sessionStoreDeleteMock(sessionId, cascade);
}
public run(sql: string, params?: unknown[]): void {
sessionStoreRunMock(sql, params);
}
},
NodeHubClient: class {
public constructor(options: unknown) {
hubClientConstructorMock(options);
}
public connect(): Promise<void> {
return hubConnectMock();
}
public subscribe(listener: unknown): () => void {
return hubSubscribeMock(listener);
}
public dispose(): Promise<void> {
return hubDisposeMock();
}
},
};
});
const profile: RemoteEnvironmentProfile = {
id: "remote-1",
name: "Build box",
host: "build.example.com",
user: "alice",
port: 2222,
createdAt: "2026-08-06T12:00:00.000Z",
updatedAt: "2026-08-06T12:00:00.000Z",
};
const connection: RemoteEnvironmentConnection = {
profile,
profileId: profile.id,
state: "connected",
endpoint: "ws://127.0.0.1:40123/hub",
authToken: "remote-hub-token",
workspaceRoot: "/home/alice",
homeDir: "/home/alice",
platform: "linux",
arch: "arm64",
remoteHubUrl: "ws://127.0.0.1:25463/hub",
localPort: 40123,
connectedAt: "2026-08-06T12:01:00.000Z",
};
const secondProfile: RemoteEnvironmentProfile = {
...profile,
id: "remote-2",
name: "Test box",
host: "test.example.com",
updatedAt: "2026-08-06T12:02:00.000Z",
};
const secondConnection: RemoteEnvironmentConnection = {
...connection,
profile: secondProfile,
profileId: secondProfile.id,
endpoint: "ws://127.0.0.1:40124/hub",
authToken: "second-remote-hub-token",
workspaceRoot: "/home/tester",
homeDir: "/home/tester",
remoteHubUrl: "ws://127.0.0.1:25464/hub",
localPort: 40124,
connectedAt: "2026-08-06T12:03:00.000Z",
};
type FakeService = {
service: RemoteEnvironmentService;
list: ReturnType<typeof vi.fn>;
upsert: ReturnType<typeof vi.fn>;
test: ReturnType<typeof vi.fn>;
connect: ReturnType<typeof vi.fn>;
disconnect: ReturnType<typeof vi.fn>;
delete: ReturnType<typeof vi.fn>;
run: ReturnType<typeof vi.fn>;
};
function createFakeService(
availableConnections: RemoteEnvironmentConnection[] = [connection],
): FakeService {
const profiles = availableConnections.map((item) => item.profile);
const availableById = new Map(
availableConnections.map((item) => [item.profileId, item]),
);
const connectedById = new Map<string, RemoteEnvironmentConnection>();
let activeProfileId: string | undefined;
const list = vi.fn(async () => profiles);
const upsert = vi.fn(async () => profile);
const test = vi.fn(
async (): Promise<RemoteEnvironmentStatus> => ({
profileId: profile.id,
state: "available",
updatedAt: "2026-08-06T12:00:30.000Z",
message: "SSH connection succeeded",
remotePlatform: "linux",
remoteArch: "arm64",
}),
);
const connect = vi.fn(async (id: string) => {
const next = availableById.get(id);
if (!next) throw new Error(`Unknown fake remote environment: ${id}`);
connectedById.set(id, next);
activeProfileId = id;
return next;
});
const disconnect = vi.fn(async (id?: string) => {
const targetId = id ?? activeProfileId;
if (!targetId) return false;
const deleted = connectedById.delete(targetId);
if (activeProfileId === targetId) activeProfileId = undefined;
return deleted;
});
const deleteProfile = vi.fn(async () => true);
const run = vi.fn(async () => ({ stdout: "", stderr: "", exitCode: 0 }));
const service = {
list,
upsert,
test,
connect,
disconnect,
delete: deleteProfile,
run,
getActive: vi.fn(() =>
activeProfileId ? connectedById.get(activeProfileId) : undefined,
),
getConnection: vi.fn((id: string) => connectedById.get(id)),
activateConnection: vi.fn((id: string) => {
if (!connectedById.has(id)) return false;
activeProfileId = id;
return true;
}),
getStatuses: vi.fn(() => []),
} as unknown as RemoteEnvironmentService;
return {
service,
list,
upsert,
test,
connect,
disconnect,
delete: deleteProfile,
run,
};
}
function createManager() {
const unsubscribe = vi.fn();
const manager = {
subscribe: vi.fn(() => unsubscribe),
dispose: vi.fn(async () => undefined),
};
return { manager, unsubscribe };
}
function attachEventRecorder(ctx: SidecarContext): ReturnType<typeof vi.fn> {
const send = vi.fn();
ctx.wsClients.add({ send });
return send;
}
function readEvent(send: ReturnType<typeof vi.fn>, index: number) {
return JSON.parse(String(send.mock.calls[index]?.[0]));
}
function createExistingRemoteBinding(
environmentId: string,
): SessionRuntimeBinding {
const sessionManager = {
dispose: vi.fn(async () => undefined),
} as unknown as SessionRuntimeBinding["sessionManager"] & {
dispose: ReturnType<typeof vi.fn>;
};
const hubClient = {
dispose: vi.fn(async () => undefined),
} as unknown as SessionRuntimeBinding["hubClient"] & {
dispose: ReturnType<typeof vi.fn>;
};
return {
environmentId,
kind: "ssh",
workspaceRoot: "/old/workspace",
sessionManager,
hubClient,
unsubscribeSessionEvents: vi.fn(),
};
}
describe("remote environment command routing", () => {
beforeEach(() => {
coreCreateMock.mockReset();
hubClientConstructorMock.mockReset();
hubConnectMock.mockReset();
hubSubscribeMock.mockReset();
hubDisposeMock.mockReset();
sessionStoreGetMock.mockReset();
sessionStoreDeleteMock.mockReset();
sessionStoreRunMock.mockReset();
hubConnectMock.mockResolvedValue(undefined);
hubSubscribeMock.mockReturnValue(() => undefined);
hubDisposeMock.mockResolvedValue(undefined);
sessionStoreGetMock.mockReturnValue(undefined);
sessionStoreDeleteMock.mockReturnValue(false);
});
it("routes list, upsert, and SSH test commands through the configured service", async () => {
const { handleCommand } = await import("./commands");
const { createSidecarContext } = await import("./context");
const fake = createFakeService();
const ctx = createSidecarContext("/local/project");
ctx.remoteEnvironments = fake.service;
await expect(
handleCommand(ctx, "list_remote_environments"),
).resolves.toEqual({
profiles: [profile],
activeEnvironmentId: "local",
activeProfileId: null,
statuses: [],
});
const input = {
id: profile.id,
name: "Build box renamed",
host: profile.host,
};
await expect(
handleCommand(ctx, "upsert_remote_environment", { profile: input }),
).resolves.toEqual({ profile });
expect(fake.upsert).toHaveBeenCalledWith(input);
await expect(
handleCommand(ctx, "test_remote_environment", { id: ` ${profile.id} ` }),
).resolves.toEqual({
profile,
status: "passed",
message: "SSH connection succeeded",
remotePlatform: "linux",
remoteArch: "arm64",
});
expect(fake.test).toHaveBeenCalledWith(profile.id);
});
it("connects an authenticated remote runtime, records its binding, and disconnects it cleanly", async () => {
const { handleCommand } = await import("./commands");
const { createSidecarContext } = await import("./context");
const fake = createFakeService();
const { manager, unsubscribe } = createManager();
coreCreateMock.mockResolvedValue(manager);
const ctx = createSidecarContext("/local/project");
ctx.remoteEnvironments = fake.service;
const send = attachEventRecorder(ctx);
await expect(
handleCommand(ctx, "connect_remote_environment", {
id: profile.id,
}),
).resolves.toEqual({
profile,
status: "connected",
environmentId: profile.id,
activeEnvironmentId: profile.id,
activeProfileId: profile.id,
workspaceRoot: "/home/alice",
homeDir: "/home/alice",
remotePlatform: "linux",
remoteArch: "arm64",
});
expect(fake.connect).toHaveBeenCalledWith(profile.id);
expect(coreCreateMock).toHaveBeenCalledWith(
expect.objectContaining({
clientName: "cline-code",
backendMode: "remote",
remote: {
endpoint: connection.endpoint,
authToken: connection.authToken,
workspaceRoot: connection.workspaceRoot,
cwd: connection.workspaceRoot,
clientType: "code-sidecar-ssh",
displayName: "Code App (Build box)",
},
}),
);
expect(hubClientConstructorMock).toHaveBeenCalledWith({
url: connection.endpoint,
authToken: connection.authToken,
clientType: "code-sidecar-ssh-observer",
displayName: "Code App observer (Build box)",
workspaceRoot: connection.workspaceRoot,
cwd: connection.workspaceRoot,
});
expect(ctx.activeEnvironmentId).toBe(profile.id);
expect(ctx.runtimeBindings.get(profile.id)).toMatchObject({
environmentId: profile.id,
kind: "ssh",
workspaceRoot: connection.workspaceRoot,
remote: connection,
});
expect(readEvent(send, 0)).toEqual({
type: "event",
event: {
name: "remote_environment_changed",
payload: {
profile,
status: "connected",
environmentId: profile.id,
activeEnvironmentId: profile.id,
activeProfileId: profile.id,
workspaceRoot: "/home/alice",
homeDir: "/home/alice",
remotePlatform: "linux",
remoteArch: "arm64",
},
},
});
await expect(
handleCommand(ctx, "disconnect_remote_environment"),
).resolves.toEqual({
status: "disconnected",
disconnectedProfileId: profile.id,
activeEnvironmentId: "local",
activeProfileId: null,
});
expect(fake.disconnect).toHaveBeenCalledWith(profile.id);
expect(unsubscribe).toHaveBeenCalledOnce();
expect(manager.dispose).toHaveBeenCalledWith(
"code_sidecar_remote_disconnect",
);
expect(hubDisposeMock).toHaveBeenCalledOnce();
expect(ctx.runtimeBindings.has(profile.id)).toBe(false);
expect(ctx.activeEnvironmentId).toBe("local");
expect(readEvent(send, 1)).toEqual({
type: "event",
event: {
name: "remote_environment_changed",
payload: {
status: "disconnected",
activeProfileId: null,
activeEnvironmentId: "local",
environmentId: "local",
workspaceRoot: "/local/project",
},
},
});
});
it("rolls back the SSH tunnel and partial runtime when observer authentication fails", async () => {
const { handleCommand } = await import("./commands");
const { createSidecarContext } = await import("./context");
const fake = createFakeService();
const { manager, unsubscribe } = createManager();
coreCreateMock.mockResolvedValue(manager);
hubConnectMock.mockRejectedValue(new Error("remote auth rejected"));
const ctx = createSidecarContext("/local/project");
ctx.remoteEnvironments = fake.service;
const send = attachEventRecorder(ctx);
await expect(
handleCommand(ctx, "connect_remote_environment", { id: profile.id }),
).rejects.toThrow("remote auth rejected");
expect(fake.disconnect).toHaveBeenCalledWith(profile.id);
expect(unsubscribe).toHaveBeenCalledOnce();
expect(manager.dispose).toHaveBeenCalledWith(
"code_sidecar_remote_initialization_failed",
);
expect(hubDisposeMock).toHaveBeenCalledOnce();
expect(ctx.runtimeBindings.has(profile.id)).toBe(false);
expect(ctx.activeEnvironmentId).toBe("local");
expect(send).not.toHaveBeenCalled();
});
it("preserves the previous environment when switching hosts fails", async () => {
const { handleCommand } = await import("./commands");
const { createSidecarContext } = await import("./context");
const fake = createFakeService([connection, secondConnection]);
const firstRuntime = createManager();
const failedRuntime = createManager();
coreCreateMock
.mockResolvedValueOnce(firstRuntime.manager)
.mockResolvedValueOnce(failedRuntime.manager);
const ctx = createSidecarContext("/local/project");
ctx.remoteEnvironments = fake.service;
const send = attachEventRecorder(ctx);
await handleCommand(ctx, "connect_remote_environment", { id: profile.id });
const firstBinding = ctx.runtimeBindings.get(profile.id);
send.mockClear();
hubConnectMock.mockRejectedValueOnce(
new Error("second host auth rejected"),
);
await expect(
handleCommand(ctx, "connect_remote_environment", {
id: secondProfile.id,
}),
).rejects.toThrow("second host auth rejected");
expect(ctx.activeEnvironmentId).toBe(profile.id);
expect(ctx.runtimeBindings.get(profile.id)).toBe(firstBinding);
expect(ctx.runtimeBindings.has(secondProfile.id)).toBe(false);
expect(fake.service.getActive()?.profileId).toBe(profile.id);
expect(fake.disconnect).toHaveBeenCalledWith(secondProfile.id);
expect(fake.disconnect).not.toHaveBeenCalledWith(profile.id);
expect(firstRuntime.unsubscribe).not.toHaveBeenCalled();
expect(firstRuntime.manager.dispose).not.toHaveBeenCalled();
expect(failedRuntime.unsubscribe).toHaveBeenCalledOnce();
expect(failedRuntime.manager.dispose).toHaveBeenCalledWith(
"code_sidecar_remote_initialization_failed",
);
expect(send).not.toHaveBeenCalled();
await expect(
handleCommand(ctx, "list_remote_environments"),
).resolves.toMatchObject({
activeEnvironmentId: profile.id,
activeProfileId: profile.id,
});
});
it("retires the previous runtime only after a host switch commits", async () => {
const { handleCommand } = await import("./commands");
const { createSidecarContext } = await import("./context");
const fake = createFakeService([connection, secondConnection]);
const firstRuntime = createManager();
const secondRuntime = createManager();
coreCreateMock
.mockResolvedValueOnce(firstRuntime.manager)
.mockResolvedValueOnce(secondRuntime.manager);
const ctx = createSidecarContext("/local/project");
ctx.remoteEnvironments = fake.service;
await handleCommand(ctx, "connect_remote_environment", { id: profile.id });
await expect(
handleCommand(ctx, "connect_remote_environment", {
id: secondProfile.id,
}),
).resolves.toMatchObject({
environmentId: secondProfile.id,
activeEnvironmentId: secondProfile.id,
activeProfileId: secondProfile.id,
});
expect(ctx.activeEnvironmentId).toBe(secondProfile.id);
expect(ctx.runtimeBindings.has(profile.id)).toBe(false);
expect(ctx.runtimeBindings.has(secondProfile.id)).toBe(true);
expect(firstRuntime.unsubscribe).toHaveBeenCalledOnce();
expect(firstRuntime.manager.dispose).toHaveBeenCalledWith(
"code_sidecar_remote_disconnect",
);
expect(secondRuntime.manager.dispose).not.toHaveBeenCalled();
expect(fake.disconnect).toHaveBeenCalledWith(profile.id);
expect(fake.service.getActive()?.profileId).toBe(secondProfile.id);
});
it("disconnecting an inactive profile does not switch the active environment", async () => {
const { handleCommand } = await import("./commands");
const { createSidecarContext } = await import("./context");
const fake = createFakeService([connection, secondConnection]);
await fake.service.connect(profile.id);
await fake.service.connect(secondProfile.id);
const ctx = createSidecarContext("/local/project");
ctx.remoteEnvironments = fake.service;
ctx.runtimeBindings.set(
profile.id,
createExistingRemoteBinding(profile.id),
);
ctx.runtimeBindings.set(
secondProfile.id,
createExistingRemoteBinding(secondProfile.id),
);
ctx.activeEnvironmentId = secondProfile.id;
const send = attachEventRecorder(ctx);
await expect(
handleCommand(ctx, "disconnect_remote_environment", { id: profile.id }),
).resolves.toEqual({
status: "disconnected",
disconnectedProfileId: profile.id,
activeEnvironmentId: secondProfile.id,
activeProfileId: secondProfile.id,
});
expect(ctx.activeEnvironmentId).toBe(secondProfile.id);
expect(ctx.runtimeBindings.has(secondProfile.id)).toBe(true);
expect(fake.service.getActive()?.profileId).toBe(secondProfile.id);
expect(send).not.toHaveBeenCalled();
});
it("deletes a profile only after removing its runtime binding", async () => {
const { handleCommand } = await import("./commands");
const { createSidecarContext } = await import("./context");
const fake = createFakeService();
const ctx = createSidecarContext("/local/project");
ctx.remoteEnvironments = fake.service;
const send = attachEventRecorder(ctx);
const binding = createExistingRemoteBinding(profile.id);
ctx.runtimeBindings.set(profile.id, binding);
ctx.activeEnvironmentId = profile.id;
await expect(
handleCommand(ctx, "delete_remote_environment", { id: profile.id }),
).resolves.toEqual({
deleted: true,
activeEnvironmentId: "local",
activeProfileId: null,
});
expect(binding.unsubscribeSessionEvents).toHaveBeenCalledOnce();
expect(binding.sessionManager.dispose).toHaveBeenCalledWith(
"code_sidecar_remote_disconnect",
);
expect(binding.hubClient.dispose).toHaveBeenCalledOnce();
expect(fake.delete).toHaveBeenCalledWith(profile.id);
expect(ctx.runtimeBindings.has(profile.id)).toBe(false);
expect(ctx.activeEnvironmentId).toBe("local");
expect(readEvent(send, 0)).toEqual({
type: "event",
event: {
name: "remote_environment_changed",
payload: {
status: "disconnected",
activeProfileId: null,
activeEnvironmentId: "local",
environmentId: "local",
workspaceRoot: "/local/project",
reason: "profile_deleted",
},
},
});
});
it("routes remote workspace browsing and operations to the explicitly selected directory", async () => {
const { handleCommand } = await import("./commands");
const { createSidecarContext } = await import("./context");
const fake = createFakeService();
const ctx = createSidecarContext("/local/project");
ctx.remoteEnvironments = fake.service;
ctx.runtimeBindings.set(
profile.id,
createExistingRemoteBinding(profile.id),
);
expect(ctx.activeEnvironmentId).toBe("local");
fake.run.mockImplementation(async (_id, input) => {
if (input.command === "pwd") {
return { stdout: "/srv/code\n", stderr: "", exitCode: 0 };
}
if (input.command === "sh") {
return {
stdout: "/srv/code/zeta\0/srv/code/project\0",
stderr: "",
exitCode: 0,
};
}
if (input.command === "git" && input.args[0] === "ls-files") {
return {
stdout: "src/remote.ts\nREADME.md\n",
stderr: "",
exitCode: 0,
};
}
if (input.command === "git" && input.args[0] === "branch") {
return { stdout: "feature/ssh\n", stderr: "", exitCode: 0 };
}
return { stdout: "main\nfeature/ssh\n", stderr: "", exitCode: 0 };
});
await expect(
handleCommand(ctx, "list_workspace_directories", {
environmentId: profile.id,
path: "/srv/code",
}),
).resolves.toEqual({
environmentId: profile.id,
currentPath: "/srv/code",
parentPath: "/srv",
entries: [
{ name: "project", path: "/srv/code/project" },
{ name: "zeta", path: "/srv/code/zeta" },
],
truncated: false,
});
expect(fake.run).toHaveBeenCalledWith(profile.id, {
command: "pwd",
args: ["-P"],
cwd: "/srv/code",
});
const listInvocation = fake.run.mock.calls.find(
([, input]) => input.command === "sh",
)?.[1];
expect(listInvocation).toMatchObject({
command: "sh",
args: [
"-c",
expect.stringContaining("find -L"),
"cline-list-workspace-directories",
"/srv/code",
],
});
expect(String(listInvocation?.args[1])).not.toContain("/srv/code");
await expect(
handleCommand(ctx, "validate_workspace_directory", {
environmentId: profile.id,
path: "/srv/code/project",
}),
).resolves.toEqual({ environmentId: profile.id, valid: true });
expect(fake.run).toHaveBeenCalledWith(profile.id, {
command: "test",
args: ["-d", "/srv/code/project"],
});
await expect(
handleCommand(ctx, "search_workspace_files", {
environmentId: profile.id,
workspaceRoot: "/srv/code/project",
query: "remote",
}),
).resolves.toEqual(["src/remote.ts"]);
expect(fake.run).toHaveBeenCalledWith(profile.id, {
command: "git",
args: ["ls-files", "--cached", "--others", "--exclude-standard"],
cwd: "/srv/code/project",
});
await expect(
handleCommand(ctx, "get_git_branch", {
environmentId: profile.id,
cwd: "/srv/code/project",
}),
).resolves.toEqual({
environmentId: profile.id,
branch: "feature/ssh",
});
expect(fake.run).toHaveBeenCalledWith(profile.id, {
command: "git",
args: ["branch", "--show-current"],
cwd: "/srv/code/project",
});
});
it("lists and bounds local workspace directories through the local binding", async () => {
const { handleCommand } = await import("./commands");
const { createSidecarContext } = await import("./context");
const temporaryRoot = mkdtempSync(join(tmpdir(), "cline-workspaces-"));
try {
for (let index = 0; index < 201; index += 1) {
mkdirSync(
join(temporaryRoot, `project-${String(index).padStart(3, "0")}`),
);
}
writeFileSync(join(temporaryRoot, "not-a-directory.txt"), "ignored");
const ctx = createSidecarContext("/local/project");
ctx.runtimeBindings.set("local", {
...createExistingRemoteBinding("local"),
kind: "local",
workspaceRoot: "/local/project",
});
const currentPath = realpathSync(temporaryRoot);
await expect(
handleCommand(ctx, "list_workspace_directories", {
environmentId: "local",
path: temporaryRoot,
}),
).resolves.toEqual({
environmentId: "local",
currentPath,
parentPath: realpathSync(tmpdir()),
entries: expect.arrayContaining([
{
name: "project-000",
path: join(currentPath, "project-000"),
},
]),
truncated: true,
});
const result = (await handleCommand(ctx, "list_workspace_directories", {
environmentId: "local",
path: temporaryRoot,
})) as { entries: unknown[] };
expect(result.entries).toHaveLength(200);
} finally {
rmSync(temporaryRoot, { recursive: true, force: true });
}
});
it("routes session reads, title updates, and deletes to the requested environment", async () => {
const { handleCommand } = await import("./commands");
const { createSidecarContext } = await import("./context");
const ctx = createSidecarContext("/local/project");
const readMessages = vi.fn(async () => [
{ role: "user", content: "remote session message" },
]);
const update = vi.fn(async () => ({ updated: true }));
const deleteSession = vi.fn(async () => true);
const sessionManager = {
get: vi.fn(async () => ({ sessionId: "remote-session", metadata: {} })),
readMessages,
update,
delete: deleteSession,
dispose: vi.fn(async () => undefined),
} as unknown as SessionRuntimeBinding["sessionManager"];
ctx.runtimeBindings.set(profile.id, {
...createExistingRemoteBinding(profile.id),
sessionManager,
});
expect(ctx.activeEnvironmentId).toBe("local");
await expect(
handleCommand(ctx, "read_session_messages", {
environmentId: profile.id,
sessionId: "remote-session",
}),
).resolves.toHaveLength(1);
expect(readMessages).toHaveBeenCalledWith("remote-session");
ctx.liveSessions.set("same-id", {
environmentId: "local",
config: {},
messages: [{ role: "user", content: "local-only message" }],
promptsInQueue: [],
busy: false,
startedAt: Date.now(),
status: "idle",
});
readMessages.mockResolvedValueOnce([]);
await expect(
handleCommand(ctx, "read_session_messages", {
environmentId: profile.id,
sessionId: "same-id",
}),
).resolves.toEqual([]);
await expect(
handleCommand(ctx, "update_chat_session_title", {
environmentId: profile.id,
sessionId: "remote-session",
title: "Remote title",
}),
).resolves.toBe(true);
expect(update).toHaveBeenCalledWith("remote-session", {
title: "Remote title",
});
await expect(
handleCommand(ctx, "delete_chat_session", {
environmentId: profile.id,
sessionId: "remote-session",
}),
).resolves.toBe(true);
expect(deleteSession).toHaveBeenCalledWith("remote-session");
expect(sessionStoreDeleteMock).not.toHaveBeenCalled();
});
});
@@ -1,9 +1,7 @@
import * as childProcess from "node:child_process";
import { EventEmitter } from "node:events";
import { mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { PassThrough } from "node:stream";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
type RemoteCommandResult,
@@ -14,11 +12,6 @@ import {
runRemoteProcess,
} from "./remote-environments";
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return { ...actual, spawn: vi.fn(actual.spawn) };
});
class FakeTunnel extends EventEmitter implements RemoteTunnelProcess {
public readonly pid = 4242;
public exitCode: number | null = null;
@@ -66,7 +59,6 @@ describe("RemoteEnvironmentService", () => {
});
afterEach(async () => {
vi.restoreAllMocks();
await rm(testDirectory, { recursive: true, force: true });
});
@@ -90,183 +82,22 @@ describe("RemoteEnvironmentService", () => {
});
}
it("waits for stream close and captures diagnostics after process exit", async () => {
const child = Object.assign(new EventEmitter(), {
stdout: new PassThrough(),
stderr: new PassThrough(),
stdin: null,
});
vi.mocked(childProcess.spawn).mockReturnValueOnce(
child as unknown as childProcess.ChildProcess,
);
const result = runRemoteProcess("ssh", [], { timeoutMs: 2000 });
child.stderr.write("gcloud NumPy warning\n");
child.emit("exit", 23, null);
child.stderr.write("remote helper failed\n");
child.emit("close", 23, null);
await expect(result).resolves.toEqual({
exitCode: 23,
stdout: "",
stderr: "gcloud NumPy warning\nremote helper failed\n",
});
});
it("drains ProxyCommand output after SSH exits", async () => {
const lateDiagnosticProgram =
'const { spawn } = require("node:child_process");' +
'spawn(process.execPath, ["-e", "setTimeout(() => process.stderr.write(\\"remote helper failed\\\\n\\"), 40)"], { stdio: ["ignore", "ignore", 2] });' +
'process.stderr.write("gcloud NumPy warning\\n");' +
"process.exit(23);";
it("captures delayed stderr from a real subprocess", async () => {
const result = await runRemoteProcess(
process.execPath,
[
"-e",
'process.stderr.write("early\\n"); setTimeout(() => { process.stderr.write("late\\n", () => { process.exitCode = 23; }); }, 40);',
],
{ timeoutMs: 5000 },
["-e", lateDiagnosticProgram],
{ timeoutMs: 2_000 },
);
expect(result).toEqual({
exitCode: 23,
stdout: "",
stderr: "early\nlate\n",
});
});
// Inherited descendant pipe handles in this fixture require POSIX.
it.skipIf(process.platform === "win32")(
"drains inherited ProxyCommand output after SSH exits",
async () => {
const lateDiagnosticProgram =
'const { spawn } = require("node:child_process");' +
'spawn(process.execPath, ["-e", "setTimeout(() => process.stderr.write(\\"remote helper failed\\\\n\\"), 40)"], { stdio: ["ignore", "ignore", 2] });' +
'process.stderr.write("gcloud NumPy warning\\n");' +
"process.exit(23);";
const result = await runRemoteProcess(
process.execPath,
["-e", lateDiagnosticProgram],
{ timeoutMs: 2_000 },
);
expect(result).toMatchObject({ exitCode: 23, stdout: "" });
expect(result.stderr).toContain("gcloud NumPy warning");
expect(result.stderr).toContain("remote helper failed");
},
);
it.each([
"stdout",
"stderr",
])("bounds captured %s and terminates a noisy process", async (stream) => {
await expect(
runRemoteProcess(
process.execPath,
[
"-e",
`process.on('SIGTERM', () => {}); setInterval(() => process.${stream}.write('x'.repeat(8192)), 1)`,
],
{ timeoutMs: 5000, maxOutputBytes: 16384 },
),
).rejects.toThrow("output exceeded 16384 bytes");
});
it("waits for SIGKILL when a timed-out process ignores SIGTERM", async () => {
const pidFile = join(testDirectory, "process.pid");
await expect(
runRemoteProcess(
process.execPath,
[
"-e",
`require('fs').writeFileSync(process.argv[1], String(process.pid)); process.on('SIGTERM', () => {}); setInterval(() => {}, 1000)`,
pidFile,
],
{ timeoutMs: 500 },
),
).rejects.toThrow("timed out");
const pid = Number(await readFile(pidFile, "utf8"));
expect(() => process.kill(pid, 0)).toThrow();
});
it("cleans up when the upload input cannot be opened", async () => {
await expect(
runRemoteProcess(process.execPath, ["-e", "process.stdin.resume()"], {
timeoutMs: 5000,
inputFile: join(testDirectory, "missing"),
}),
).rejects.toThrow("ENOENT");
});
it.each([
"connect",
"delete",
])("recovers lost-Hub cleanup with a deleted helper before %s", async (action) => {
const commands: string[] = [];
const tunnels: FakeTunnel[] = [];
let offline = false;
let requiredIdentity: string | undefined;
let helperMissing = false;
let uploads = 0;
const dependencies: Partial<RemoteEnvironmentDependencies> = {
runProcess: async (_executable, args, options) => {
const command = args.at(-1) ?? "";
commands.push(command);
if (offline) throw new Error("Network unavailable");
if (options.inputFile) {
uploads += 1;
helperMissing = false;
}
if (helperMissing && command.includes("'test' '-x'"))
return { stdout: "", stderr: "", exitCode: 1 };
if (helperMissing && command.includes("--remote-hub-stop"))
throw new Error("Helper missing");
if (requiredIdentity && !args.includes(requiredIdentity)) {
throw new Error("Obsolete SSH identity");
}
if (command.includes("uname -s"))
return inspection("Linux", "x86_64", "/home/dev");
if (command.includes("--remote-hub-ensure"))
return success(
'{"url":"ws://127.0.0.1:25463/hub","authToken":"token"}',
);
return success();
},
resolveHelperBinary: async () => "/helper",
fileReadable: async () => true,
hashFile: async () => "0123456789abcdef",
reservePort: async () => 41000 + tunnels.length,
spawnTunnel: () => {
const tunnel = new FakeTunnel();
tunnels.push(tunnel);
return tunnel;
},
waitForTunnel: async () => undefined,
};
const service = createService(dependencies);
const first = await service.upsert({ name: "First", host: "same-account" });
const second = await service.upsert({
name: "Second",
host: "same-account",
});
await service.connect(first.id);
await service.connect(second.id);
const ensures = commands.filter((command) =>
command.includes("--remote-hub-ensure"),
);
expect(ensures).toHaveLength(2);
expect(ensures[0]).not.toBe(ensures[1]);
offline = true;
tunnels[0].emit("exit", 255, null);
await expect(service.dispose()).rejects.toThrow("Network unavailable");
expect(await readdir(`${profilesPath}.cleanup`)).toHaveLength(1);
offline = false;
const restarted = createService(dependencies);
requiredIdentity = "/keys/rotated";
helperMissing = true;
await restarted.upsert({ ...first, identityFile: requiredIdentity });
if (action === "connect") await restarted.connect(first.id);
else expect(await restarted.delete(first.id)).toBe(true);
expect(uploads).toBe(1);
await restarted.dispose();
expect(await readdir(`${profilesPath}.cleanup`)).toEqual([]);
const stop = commands
.filter((command) => command.includes("--remote-hub-stop"))
.at(-1);
expect(stop?.split("'--discovery-path' ")[1]).toBe(
ensures[0]?.split("'--discovery-path' ")[1],
);
expect(result).toMatchObject({ exitCode: 23, stdout: "" });
expect(result.stderr).toContain("gcloud NumPy warning");
expect(result.stderr).toContain("remote helper failed");
});
it("persists profiles atomically with private permissions and updates in place", async () => {
@@ -286,10 +117,7 @@ describe("RemoteEnvironmentService", () => {
createdAt: "2026-08-06T12:00:00.000Z",
updatedAt: "2026-08-06T12:00:00.000Z",
});
// Windows exposes synthetic mode bits; file access is governed by ACLs.
if (process.platform !== "win32") {
expect((await stat(profilesPath)).mode & 0o777).toBe(0o600);
}
expect((await stat(profilesPath)).mode & 0o777).toBe(0o600);
const stored = JSON.parse(await readFile(profilesPath, "utf8"));
expect(stored).toEqual({ version: 1, profiles: [created] });
@@ -350,7 +178,7 @@ describe("RemoteEnvironmentService", () => {
"-o",
"BatchMode=yes",
"ConnectTimeout=10",
"StrictHostKeyChecking=yes",
"StrictHostKeyChecking=accept-new",
"-p",
"2202",
"-i",
@@ -447,12 +275,7 @@ describe("RemoteEnvironmentService", () => {
host: "arm-builder",
});
const [connection, concurrentConnection] = await Promise.all([
service.connect(profile.id),
service.connect(profile.id),
]);
expect(concurrentConnection).toEqual(connection);
expect(spawnTunnel).toHaveBeenCalledTimes(1);
const connection = await service.connect(profile.id);
expect(connection).toMatchObject({
profile,
profileId: profile.id,
@@ -481,8 +304,8 @@ describe("RemoteEnvironmentService", () => {
invocation.args.at(-1)?.includes("--remote-hub-ensure"),
);
expect(ensure?.args.at(-1)).toContain("'/home/dev'");
expect(ensure?.args.at(-1)).toMatch(
/\/home\/dev\/\.cline\/data\/remote\/[a-f0-9-]+\.json/,
expect(ensure?.args.at(-1)).toContain(
"'/home/dev/.cline/data/remote/desktop-hub.json'",
);
expect(spawnTunnel).toHaveBeenCalledWith(
"ssh",
@@ -505,55 +328,6 @@ describe("RemoteEnvironmentService", () => {
expect(service.getActive()).toBeUndefined();
});
it.each([
"reserve",
"spawn",
"ready",
])("cleans up the owned remote Hub when %s fails", async (stage) => {
const commands: string[] = [];
const tunnel = new FakeTunnel();
const fail = () => {
throw new Error("tunnel setup failed");
};
const service = createService({
runProcess: async (_executable, args) => {
const command = args.at(-1) ?? "";
commands.push(command);
if (command.includes("uname -s"))
return inspection("Linux", "aarch64", "/home/dev");
if (command.includes("--remote-hub-ensure"))
return success(
'{"url":"ws://127.0.0.1:25463/hub","authToken":"secret"}',
);
return success();
},
resolveHelperBinary: async () => "/helper",
fileReadable: async () => true,
hashFile: async () => "abcdef0123456789fedcba9876543210",
reservePort: async () => (stage === "reserve" ? fail() : 43117),
spawnTunnel: () => (stage === "spawn" ? fail() : tunnel),
waitForTunnel: async () => {
if (stage === "ready") fail();
},
});
const profile = await service.upsert({ name: "Remote", host: "remote" });
await expect(service.connect(profile.id)).rejects.toThrow(
"tunnel setup failed",
);
const ensure = commands.find((command) =>
command.includes("--remote-hub-ensure"),
);
const stop = commands.find((command) =>
command.includes("--remote-hub-stop"),
);
expect(stop).toBeDefined();
expect(stop?.split("'--discovery-path' ")[1]).toBe(
ensure?.split("'--discovery-path' ")[1],
);
expect(tunnel.killed).toBe(stage === "ready");
expect(service.getConnection(profile.id)).toBeUndefined();
});
it("bounds Hub shutdown before closing the SSH tunnel", async () => {
const tunnel = new FakeTunnel();
const requestHubShutdown = vi.fn(
@@ -716,7 +490,7 @@ describe("RemoteEnvironmentService", () => {
});
await expect(service.connect(profile.id)).rejects.toThrow(
"unsupported in SSH: no compatible remote helper binary",
"unsupported in SSH v0: no compatible desktop helper binary",
);
expect(invocations.some((invocation) => invocation.options.inputFile)).toBe(
false,
@@ -871,8 +645,8 @@ describe("RemoteEnvironmentService", () => {
state: "error",
}),
);
// Cleanup uses a fresh SSH connection, never the failed local tunnel.
await service.dispose();
// The tunnel is already gone, so leave the dedicated owner record for a
// later reconnect instead of risking shutdown of an unrelated local Hub.
expect(requestHubShutdown).not.toHaveBeenCalled();
});
@@ -6,17 +6,15 @@ import {
chmod,
mkdir,
open,
readdir,
readFile,
rename,
rm,
writeFile,
} from "node:fs/promises";
import { createConnection, createServer } from "node:net";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { basename, dirname, join } from "node:path";
import { requestHubShutdown } from "@cline/core";
import { resolveClineDataDir } from "@cline/shared/storage";
import { requestHubShutdown } from "../hub/client";
export interface RemoteEnvironmentProfile {
id: string;
@@ -102,8 +100,6 @@ export interface RemoteTunnelProcess {
export interface RemoteProcessOptions {
timeoutMs: number;
inputFile?: string;
/** Combined stdout/stderr limit; defaults to 8 MiB. */
maxOutputBytes?: number;
}
interface RemoteInspection extends RemoteHelperTarget {
@@ -153,14 +149,7 @@ interface ProfilesFile {
profiles: RemoteEnvironmentProfile[];
}
interface PendingHubCleanup {
profile: RemoteEnvironmentProfile;
helper: string;
discoveryPath: string;
}
interface ManagedConnection {
cleanup: PendingHubCleanup;
connection: RemoteEnvironmentConnection;
tunnel: RemoteTunnelProcess;
}
@@ -171,13 +160,12 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
const DEFAULT_UPLOAD_TIMEOUT_MS = 5 * 60_000;
const DEFAULT_TUNNEL_TIMEOUT_MS = 10_000;
const DEFAULT_HUB_SHUTDOWN_TIMEOUT_MS = 2_000;
const REMOTE_HELPER_DIRECTORY = ".cline/remote";
const REMOTE_DISCOVERY_DIRECTORY = ".cline/data/remote";
const REMOTE_HELPER_DIRECTORY = ".cline/code/remote";
const REMOTE_DISCOVERY_PATH = ".cline/data/remote/desktop-hub.json";
const REMOTE_INSPECTION_SENTINEL = "CLINE_REMOTE_INSPECT_V1";
export class RemoteEnvironmentService {
private readonly profilesPath: string;
private readonly ownerId = randomUUID();
private readonly sshPath: string;
private readonly knownHostsPath?: string;
private readonly connectTimeoutSeconds: number;
@@ -287,7 +275,7 @@ export class RemoteEnvironmentService {
return false;
}
await this.disconnectProfile(id);
await this.disconnect(id);
await this.writeProfiles(remaining);
this.statuses.delete(id);
return true;
@@ -310,19 +298,8 @@ export class RemoteEnvironmentService {
}
}
public connect(id: string): Promise<RemoteEnvironmentConnection> {
return this.withMutation(() => this.connectProfile(id));
}
private async connectProfile(
id: string,
): Promise<RemoteEnvironmentConnection> {
public async connect(id: string): Promise<RemoteEnvironmentConnection> {
const profile = await this.requireProfile(id);
// upsert() rejects host/user/port changes via assertProfileUpdateAllowed,
// even while disconnected. A different destination needs a new profile,
// so editing this profile cannot redirect it while old cleanup is pending.
// Finish cleanup before starting another Hub; retries use the current SSH key.
await this.retryPendingCleanup(id);
const previousManaged = this.connections.get(id);
const current = previousManaged?.connection;
if (current && profilesUseSameConnection(current.profile, profile)) {
@@ -331,18 +308,39 @@ export class RemoteEnvironmentService {
}
this.setStatus(id, "connecting", "Connecting to remote environment");
let bootstrap: { helper: string; discoveryPath: string } | undefined;
let pendingTunnel: RemoteTunnelProcess | undefined;
try {
const inspection = await this.inspectRemote(profile);
await this.validateDirectory(profile, inspection.home);
const remoteHelper = await this.ensureHelper(profile, inspection);
const localHelper =
await this.dependencies.resolveHelperBinary(inspection);
if (
!localHelper ||
!(await this.dependencies.fileReadable(localHelper))
) {
throw new Error(
`Remote target ${inspection.platform}/${inspection.arch} is unsupported in SSH v0: ` +
"no compatible desktop helper binary is available. Build the matching desktop sidecar and set " +
"CLINE_REMOTE_HELPER_BINARY; network installers are intentionally not used.",
);
}
const discoveryPath = joinRemote(
const hash = await this.dependencies.hashFile(localHelper);
const remoteDirectory = joinRemote(
inspection.home,
`${REMOTE_DISCOVERY_DIRECTORY}/${this.ownerId}-${createHash("sha256").update(profile.id).digest("hex").slice(0, 16)}.json`,
REMOTE_HELPER_DIRECTORY,
);
bootstrap = { helper: remoteHelper, discoveryPath };
const remoteHelper = joinRemote(
remoteDirectory,
`cline-desktop-helper-${inspection.platform}-${inspection.arch}-${hash.slice(0, 16)}`,
);
await this.installHelper(
profile,
localHelper,
remoteDirectory,
remoteHelper,
);
const discoveryPath = joinRemote(inspection.home, REMOTE_DISCOVERY_PATH);
const ensureResult = await this.execRemote(profile, {
command: remoteHelper,
args: [
@@ -359,13 +357,17 @@ export class RemoteEnvironmentService {
this.sshPath,
this.buildTunnelArgs(profile, localPort, hub.port),
);
pendingTunnel = tunnel;
await this.dependencies.waitForTunnel(
localPort,
tunnel,
this.tunnelTimeoutMs,
);
try {
await this.dependencies.waitForTunnel(
localPort,
tunnel,
this.tunnelTimeoutMs,
);
} catch (error) {
tunnel.kill("SIGTERM");
throw error;
}
const connectedAt = this.dependencies.now().toISOString();
const connection: RemoteEnvironmentConnection = {
@@ -382,11 +384,7 @@ export class RemoteEnvironmentService {
localPort,
connectedAt,
};
const managed = {
connection,
tunnel,
cleanup: { profile, helper: remoteHelper, discoveryPath },
};
const managed = { connection, tunnel };
this.connections.set(id, managed);
this.activeProfileId = id;
this.setStatus(id, "connected", "Connected", inspection);
@@ -405,53 +403,20 @@ export class RemoteEnvironmentService {
}
previousManaged.tunnel.kill("SIGTERM");
}
bootstrap = undefined;
pendingTunnel = undefined;
return { ...connection, profile: { ...connection.profile } };
} catch (error) {
pendingTunnel?.kill("SIGTERM");
let cleanupError: unknown;
if (bootstrap) {
try {
// An independent SSH command works even when forwarding never opened.
await this.execRemote(profile, {
command: bootstrap.helper,
args: [
"--remote-hub-stop",
"--discovery-path",
bootstrap.discoveryPath,
],
});
} catch (failure) {
cleanupError = failure;
await this.persistPendingCleanup({ profile, ...bootstrap });
}
}
if (cleanupError) {
const failure = new Error(
`${errorMessage(error)}; remote Hub cleanup failed: ${errorMessage(cleanupError)}`,
{ cause: error },
);
this.setStatus(id, "error", failure.message);
throw failure;
}
this.setStatus(id, "error", errorMessage(error));
throw error;
}
}
public disconnect(id?: string): Promise<boolean> {
return this.withMutation(() => this.disconnectProfile(id));
}
private async disconnectProfile(id?: string): Promise<boolean> {
public async disconnect(id?: string): Promise<boolean> {
const targetId = id ?? this.activeProfileId;
if (!targetId) {
return false;
}
const managed = this.connections.get(targetId);
if (!managed) {
await this.retryPendingCleanup(targetId);
if (this.activeProfileId === targetId) {
this.activeProfileId = undefined;
}
@@ -484,7 +449,7 @@ export class RemoteEnvironmentService {
/**
* Marks an already-established tunnel as active without doing any SSH work.
* A client uses this to roll back a host switch when the
* The desktop command layer uses this to roll back a host switch when the
* new Hub runtime cannot be initialized.
*/
public activateConnection(id: string): boolean {
@@ -520,44 +485,10 @@ export class RemoteEnvironmentService {
return this.execRemote(profile, { ...input, cwd });
}
public dispose(): Promise<void> {
return this.withMutation(async () => {
for (const id of [...this.connections.keys()]) {
await this.disconnectProfile(id);
}
await this.retryPendingCleanup();
});
}
private async ensureHelper(
profile: RemoteEnvironmentProfile,
inspection: RemoteInspection,
): Promise<string> {
const localHelper = await this.dependencies.resolveHelperBinary(inspection);
if (!localHelper || !(await this.dependencies.fileReadable(localHelper))) {
throw new Error(
`Remote target ${inspection.platform}/${inspection.arch} is unsupported in SSH: ` +
"no compatible remote helper binary is available. Build the matching core remote helper and set " +
"CLINE_REMOTE_HELPER_BINARY; network installers are intentionally not used.",
);
public async dispose(): Promise<void> {
for (const id of [...this.connections.keys()]) {
await this.disconnect(id);
}
const hash = await this.dependencies.hashFile(localHelper);
const remoteDirectory = joinRemote(
inspection.home,
REMOTE_HELPER_DIRECTORY,
);
const remoteHelper = joinRemote(
remoteDirectory,
`cline-remote-helper-${inspection.platform}-${inspection.arch}-${hash.slice(0, 16)}`,
);
await this.installHelper(
profile,
localHelper,
remoteDirectory,
remoteHelper,
);
return remoteHelper;
}
private async installHelper(
@@ -590,7 +521,7 @@ export class RemoteEnvironmentService {
{ timeoutMs: this.uploadTimeoutMs, inputFile: localHelper },
);
if (upload.exitCode !== 0) {
throw processFailure("upload remote helper", upload);
throw processFailure("upload desktop helper", upload);
}
try {
await this.execRemote(profile, {
@@ -651,7 +582,7 @@ export class RemoteEnvironmentService {
const arch = normalizeArch(rawArch?.trim());
if (!platform || !arch) {
throw new Error(
`Remote target ${rawPlatform || "unknown"}/${rawArch || "unknown"} is unsupported in SSH; ` +
`Remote target ${rawPlatform || "unknown"}/${rawArch || "unknown"} is unsupported in SSH v0; ` +
"only Linux and macOS on x64 or arm64 are supported.",
);
}
@@ -707,7 +638,7 @@ export class RemoteEnvironmentService {
"-o",
`ConnectTimeout=${this.connectTimeoutSeconds}`,
"-o",
"StrictHostKeyChecking=yes",
"StrictHostKeyChecking=accept-new",
];
if (profile.port) {
args.push("-p", String(profile.port));
@@ -792,88 +723,10 @@ export class RemoteEnvironmentService {
if (this.activeProfileId === id) {
this.activeProfileId = undefined;
}
// Persist before retrying: network loss can outlive this client process.
void this.withMutation(async () => {
await this.persistPendingCleanup(managed.cleanup);
await this.retryPendingCleanup(id);
}).catch((error) =>
this.setStatus(
id,
"error",
`${message}; remote Hub cleanup pending: ${errorMessage(error)}`,
),
);
const status = this.setStatus(id, "error", message);
this.onConnectionLost?.(status);
}
private async persistPendingCleanup(
cleanup: PendingHubCleanup,
): Promise<void> {
const directory = `${this.profilesPath}.cleanup`;
await mkdir(directory, { recursive: true, mode: 0o700 });
const key = createHash("sha256")
.update(cleanup.discoveryPath)
.digest("hex");
const path = join(directory, `${key}.json`);
const temporary = `${path}.${randomUUID()}.tmp`;
try {
await writeFile(temporary, JSON.stringify(cleanup), { mode: 0o600 });
await rename(temporary, path);
} finally {
await rm(temporary, { force: true });
}
}
private async retryPendingCleanup(profileId?: string): Promise<void> {
const directory = `${this.profilesPath}.cleanup`;
let files: string[];
try {
files = await readdir(directory);
} catch (error) {
if (isNodeError(error) && error.code === "ENOENT") return;
throw error;
}
const profiles = await this.readProfiles();
for (const file of files.filter((file) => file.endsWith(".json"))) {
const path = join(directory, file);
const cleanup: PendingHubCleanup = JSON.parse(
await readFile(path, "utf8"),
);
if (profileId && cleanup.profile.id !== profileId) continue;
// An identity-file edit is allowed while disconnected; destination edits
// are rejected by assertProfileUpdateAllowed. Use the current credentials
// for retries, but still verify the destination before reusing them from
// persisted state (the profile may have been removed or the file edited).
const profile = profiles.find(
(profile) =>
profile.id === cleanup.profile.id &&
profile.host === cleanup.profile.host &&
profile.user === cleanup.profile.user &&
profile.port === cleanup.profile.port,
);
const cleanupProfile = profile ?? cleanup.profile;
const available = await this.execRemoteAllowFailure(cleanupProfile, {
command: "test",
args: ["-x", cleanup.helper],
});
// Cache removal must not strand the durable cleanup record. Restore
// a compatible helper, still targeting the original owned discovery.
const helper =
available.exitCode === 0
? cleanup.helper
: await this.ensureHelper(
cleanupProfile,
await this.inspectRemote(cleanupProfile),
);
await this.execRemote(cleanupProfile, {
command: helper,
args: ["--remote-hub-stop", "--discovery-path", cleanup.discoveryPath],
});
await rm(path, { force: true });
}
}
private async readProfiles(): Promise<RemoteEnvironmentProfile[]> {
let raw: string;
try {
@@ -953,13 +806,32 @@ function createDefaultDependencies(
if (configuredHelper) {
return configuredHelper;
}
const filename = remoteHelperBinaryFilename(target);
const filename = helperBinaryFilename(target);
const executableDirectory = dirname(process.execPath);
const candidates = [
...(configuredHelperDirectory
? [join(configuredHelperDirectory, filename)]
: []),
join(executableDirectory, "remote-helpers", filename),
join(
executableDirectory,
"..",
"Resources",
"bin",
"remote-helpers",
filename,
),
join(process.cwd(), "src-tauri", "bin", "remote-helpers", filename),
join(
process.cwd(),
"apps",
"examples",
"desktop-app",
"src-tauri",
"bin",
"remote-helpers",
filename,
),
];
for (const candidate of candidates) {
try {
@@ -969,6 +841,16 @@ function createDefaultDependencies(
// Try the next packaged/development resource location.
}
}
// A packaged Linux desktop contains the smaller dedicated helper for its
// own architecture, so only fall back to the full sidecar executable after
// checking resources. The fallback still enables same-host macOS SSH.
if (
target.platform === process.platform &&
target.arch === process.arch &&
basename(process.execPath).startsWith("code-sidecar")
) {
return process.execPath;
}
return undefined;
},
fileReadable: async (path) => {
@@ -985,7 +867,7 @@ function createDefaultDependencies(
};
}
export function remoteHelperBinaryFilename(target: RemoteHelperTarget): string {
function helperBinaryFilename(target: RemoteHelperTarget): string {
const triple =
target.platform === "darwin"
? target.arch === "arm64"
@@ -994,7 +876,7 @@ export function remoteHelperBinaryFilename(target: RemoteHelperTarget): string {
: target.arch === "arm64"
? "aarch64-unknown-linux-gnu"
: "x86_64-unknown-linux-gnu";
return `cline-remote-helper-${triple}`;
return `code-sidecar-${triple}`;
}
export async function runRemoteProcess(
@@ -1002,107 +884,61 @@ export async function runRemoteProcess(
args: string[],
options: RemoteProcessOptions,
): Promise<RemoteCommandResult> {
const maxOutputBytes = options.maxOutputBytes ?? 8 * 1024 * 1024;
if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1)
throw new Error("maxOutputBytes must be a positive integer");
return new Promise((resolve, reject) => {
const child = spawn(executable, args, {
stdio: [options.inputFile ? "pipe" : "ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
});
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
let bytes = 0;
let settled = false;
let failure: Error | undefined;
let input: ReturnType<typeof createReadStream> | undefined;
let escalation: ReturnType<typeof setTimeout> | undefined;
let deadline: ReturnType<typeof setTimeout> | undefined;
const stopInput = () => {
input?.destroy();
child.stdin?.destroy();
};
const signalTree = (signal: NodeJS.Signals) => {
if (!child.pid) return;
if (process.platform === "win32") {
// taskkill includes ProxyCommand descendants; child.kill alone does not.
const killer = spawn(
"taskkill",
["/pid", String(child.pid), "/T", "/F"],
{ stdio: "ignore" },
);
killer.on("error", () => child.kill(signal));
} else {
try {
process.kill(-child.pid, signal);
} catch {
child.kill(signal);
}
}
};
const finish = (error?: Error, code?: number) => {
if (settled) return;
settled = true;
clearTimeout(timer);
clearTimeout(escalation);
clearTimeout(deadline);
stopInput();
child.stdout?.destroy();
child.stderr?.destroy();
if (error) reject(error);
else
resolve({
stdout: Buffer.concat(stdout).toString("utf8"),
stderr: Buffer.concat(stderr).toString("utf8"),
exitCode: code ?? 1,
});
};
const terminate = (error: Error) => {
if (settled || failure) return;
failure = error;
stopInput();
signalTree("SIGTERM");
escalation = setTimeout(() => {
signalTree("SIGKILL");
// An inherited pipe must not keep cleanup waiting forever.
deadline = setTimeout(() => finish(failure), 500);
}, 500);
};
const timer = setTimeout(
() =>
terminate(
new Error(`${executable} timed out after ${options.timeoutMs}ms`),
),
options.timeoutMs,
);
const capture = (target: Buffer[], chunk: Buffer) => {
if (settled || failure) return;
bytes += chunk.length;
if (bytes > maxOutputBytes) {
terminate(
new Error(`${executable} output exceeded ${maxOutputBytes} bytes`),
);
const timer = setTimeout(() => {
child.kill("SIGTERM");
finish(new Error(`${executable} timed out after ${options.timeoutMs}ms`));
}, options.timeoutMs);
const finish = (error?: Error, exitCode?: number): void => {
if (settled) {
return;
}
target.push(chunk);
settled = true;
clearTimeout(timer);
if (error) {
reject(error);
return;
}
resolve({
stdout: Buffer.concat(stdout).toString("utf8"),
stderr: Buffer.concat(stderr).toString("utf8"),
exitCode: exitCode ?? 1,
});
};
child.stdout?.on("data", (chunk: Buffer) => capture(stdout, chunk));
child.stderr?.on("data", (chunk: Buffer) => capture(stderr, chunk));
child.once("error", (error) => terminate(error));
// Drain late ProxyCommand diagnostics before resolving a successful command.
child.once("close", (code, signal) =>
finish(
failure ??
(signal
? new Error(`${executable} exited from signal ${signal}`)
: undefined),
code ?? 1,
),
);
child.stdout?.on("data", (chunk: Buffer) => stdout.push(chunk));
child.stderr?.on("data", (chunk: Buffer) => stderr.push(chunk));
child.once("error", (error) => finish(error));
// `exit` can fire before stdout/stderr pipes are fully drained. This is
// especially visible with SSH ProxyCommand processes (for example gcloud
// IAP): an early proxy warning arrives before the remote helper's actual
// diagnostic. Settle on `close`, which Node emits after every stdio stream
// belonging to the child has closed.
child.once("close", (code, signal) => {
if (signal) {
finish(new Error(`${executable} exited from signal ${signal}`));
return;
}
finish(undefined, code ?? 1);
});
if (options.inputFile && child.stdin) {
input = createReadStream(options.inputFile);
child.stdin.once("error", terminate);
input.once("error", terminate);
const input = createReadStream(options.inputFile);
child.stdin.once("error", (error) => {
child.kill("SIGTERM");
finish(error);
});
input.once("error", (error) => {
child.kill("SIGTERM");
finish(error);
});
input.pipe(child.stdin);
}
});
@@ -1353,16 +1189,18 @@ function parseRemoteHubResult(stdout: string): {
parsed = JSON.parse(lines.at(-1) ?? "");
} catch (error) {
throw new Error(
"Remote remote helper returned invalid hub discovery JSON",
"Remote desktop helper returned invalid hub discovery JSON",
{ cause: error },
);
}
if (!parsed || typeof parsed !== "object") {
throw new Error("Remote remote helper returned invalid hub discovery data");
throw new Error(
"Remote desktop helper returned invalid hub discovery data",
);
}
const record = parsed as Record<string, unknown>;
if (typeof record.authToken !== "string" || !record.authToken) {
throw new Error("Remote remote helper did not return a hub auth token");
throw new Error("Remote desktop helper did not return a hub auth token");
}
let port = typeof record.port === "number" ? record.port : undefined;
@@ -1372,7 +1210,7 @@ function parseRemoteHubResult(stdout: string): {
try {
url = new URL(record.url);
} catch (error) {
throw new Error("Remote remote helper returned an invalid hub URL", {
throw new Error("Remote desktop helper returned an invalid hub URL", {
cause: error,
});
}
@@ -1380,7 +1218,7 @@ function parseRemoteHubResult(stdout: string): {
!isLoopbackHost(url.hostname) ||
!["ws:", "wss:", "http:", "https:"].includes(url.protocol)
) {
throw new Error("Remote remote helper hub URL must use a loopback host");
throw new Error("Remote desktop helper hub URL must use a loopback host");
}
port = Number(
url.port ||
@@ -1389,10 +1227,10 @@ function parseRemoteHubResult(stdout: string): {
pathname = url.pathname || "/hub";
}
if (!Number.isInteger(port) || (port ?? 0) < 1 || (port ?? 0) > 65_535) {
throw new Error("Remote remote helper returned an invalid hub port");
throw new Error("Remote desktop helper returned an invalid hub port");
}
if (!pathname.startsWith("/") || /[\0\r\n]/.test(pathname)) {
throw new Error("Remote remote helper returned an invalid hub path");
throw new Error("Remote desktop helper returned an invalid hub path");
}
return { port: port as number, pathname, authToken: record.authToken };
}
@@ -0,0 +1,95 @@
import { describe, expect, it, vi } from "vitest";
import {
type RemoteHelperDependencies,
runRemoteHelperEntrypoint,
} from "./remote-helper";
function createDependencies(
overrides: Partial<RemoteHelperDependencies> = {},
): {
dependencies: RemoteHelperDependencies;
output: string[];
} {
const output: string[] = [];
return {
output,
dependencies: {
ensureDetachedHubServer: vi.fn(async () => ({
url: "ws://127.0.0.1:25463/hub",
authToken: "desktop-owner-token",
})),
claimHubDaemonProcess: vi.fn(() => false),
loadHubDaemon: vi.fn(async () => undefined),
ensureLoginShellPath: vi.fn(async () => ({
status: "skipped" as const,
reason: "test",
})),
setHomeDirIfUnset: vi.fn(),
homeDir: () => "/home/pi",
cwd: () => "/home/pi",
env: {},
writeOutput: (value) => output.push(value),
...overrides,
},
};
}
describe("remote helper entrypoint", () => {
it("starts only the explicitly owned desktop Hub discovery record", async () => {
const { dependencies, output } = createDependencies();
const discoveryPath = "/home/pi/.cline/data/remote/desktop-hub.json";
await expect(
runRemoteHelperEntrypoint(
[
"code-sidecar",
"--remote-hub-ensure",
"--cwd",
"/home/pi",
"--discovery-path",
discoveryPath,
],
dependencies,
),
).resolves.toBe(true);
expect(dependencies.env.CLINE_HUB_DISCOVERY_PATH).toBe(discoveryPath);
expect(dependencies.ensureDetachedHubServer).toHaveBeenCalledWith(
"/home/pi",
{
host: "127.0.0.1",
port: 0,
pathname: "/hub",
allowPortFallback: true,
},
);
expect(JSON.parse(output.join(""))).toMatchObject({
url: "ws://127.0.0.1:25463/hub",
authToken: "desktop-owner-token",
cwd: "/home/pi",
});
});
it("refuses bootstrap without an explicit discovery owner", async () => {
const { dependencies } = createDependencies();
await expect(
runRemoteHelperEntrypoint(
["code-sidecar", "--remote-hub-ensure"],
dependencies,
),
).rejects.toThrow("--discovery-path is required");
});
it("hosts the detached daemon when the one-shot sentinel is claimed", async () => {
const loadHubDaemon = vi.fn(async () => undefined);
const { dependencies } = createDependencies({
claimHubDaemonProcess: () => true,
loadHubDaemon,
});
await expect(
runRemoteHelperEntrypoint(["code-sidecar"], dependencies),
).resolves.toBe(true);
expect(loadHubDaemon).toHaveBeenCalledOnce();
});
});
@@ -1,16 +1,9 @@
import { homedir } from "node:os";
import { ensureDetachedHubServer, setHomeDirIfUnset } from "@cline/core";
import { claimHubDaemonProcess } from "@cline/shared";
import { setHomeDirIfUnset } from "@cline/shared/storage";
import { requestHubShutdown } from "../hub/client";
import { ensureDetachedHubServer } from "../hub/daemon";
import { clearHubDiscoveryIfOwned, readHubDiscovery } from "../hub/discovery";
import { ensureLoginShellPath } from "./shell-path";
export type RemoteHelperDependencies = {
readHubDiscovery: typeof readHubDiscovery;
clearHubDiscoveryIfOwned: typeof clearHubDiscoveryIfOwned;
probeProcess: (pid: number) => void;
requestHubShutdown: typeof requestHubShutdown;
ensureDetachedHubServer: typeof ensureDetachedHubServer;
claimHubDaemonProcess: typeof claimHubDaemonProcess;
loadHubDaemon: () => Promise<unknown>;
@@ -23,12 +16,6 @@ export type RemoteHelperDependencies = {
};
const defaultDependencies: RemoteHelperDependencies = {
readHubDiscovery,
clearHubDiscoveryIfOwned,
probeProcess: (pid) => {
process.kill(pid, 0);
},
requestHubShutdown,
ensureDetachedHubServer,
claimHubDaemonProcess,
loadHubDaemon: () => import("@cline/core/hub/daemon-entry"),
@@ -73,7 +60,6 @@ export async function runRemoteHubEnsure(
port: 0,
pathname: "/hub",
allowPortFallback: true,
manageConnectors: false,
});
dependencies.writeOutput(
`${JSON.stringify({
@@ -87,43 +73,14 @@ export async function runRemoteHubEnsure(
/**
* Handles the SSH bootstrap command and the detached-daemon sentinel. The
* standalone helper is compiled for the target host and contains no client UI
* server or command router. Client executables may also use this entrypoint
* to support the daemon sentinel.
* full desktop sidecar imports this function for same-platform remote hosts;
* packaged Linux SSH helpers compile this file directly and contain no desktop
* HTTP/WebSocket server or UI command router.
*/
export async function runRemoteHelperEntrypoint(
argv = process.argv,
dependencies: RemoteHelperDependencies = defaultDependencies,
): Promise<boolean> {
if (argv.includes("--remote-hub-stop")) {
const discoveryPath = configureDedicatedDiscovery(argv, dependencies);
const hub = await dependencies.readHubDiscovery(discoveryPath);
// A crash or reboot can leave discovery pointing at a dead Hub. Only
// ESRCH proves the process is gone; permission/probe errors and live
// processes must still go through authenticated shutdown.
if (
hub &&
typeof hub.pid === "number" &&
Number.isInteger(hub.pid) &&
hub.pid > 0
) {
try {
dependencies.probeProcess(hub.pid);
} catch (error) {
if ((error as NodeJS.ErrnoException)?.code === "ESRCH") {
await dependencies.clearHubDiscoveryIfOwned(discoveryPath, hub.hubId);
return true;
}
}
}
if (
hub &&
!(await dependencies.requestHubShutdown(hub.url, hub.authToken))
) {
throw new Error("Remote Hub shutdown failed");
}
return true;
}
if (argv.includes("--remote-hub-ensure")) {
await runRemoteHubEnsure(argv, dependencies);
return true;
@@ -136,3 +93,15 @@ export async function runRemoteHelperEntrypoint(
}
return false;
}
if (import.meta.main) {
void (async () => {
if (!(await runRemoteHelperEntrypoint())) {
throw new Error("A remote helper command is required");
}
})().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
});
}
@@ -41,6 +41,21 @@ describe("restore_checkpoint", () => {
// makes the read after a restore prefer the file over the live session.
persistSessionMessages(sessionId, fullMessages);
const sessionManager = {
get: vi.fn(async () => ({
sessionId,
status: "idle",
cwd: "/tmp/project",
workspaceRoot: "/tmp/project",
})),
// A restore that reuses the source id is what the hub does today.
restore: vi.fn(async () => ({
sessionId,
messages: restoredMessages,
checkpoint: { ref: "first", createdAt: 1, runCount: 1 },
})),
pendingPrompts: { list: vi.fn(async () => []) },
};
const ctx = {
liveSessions: new Map([
[
@@ -60,21 +75,23 @@ describe("restore_checkpoint", () => {
pendingQuestions: new Map(),
streamIndices: new Map(),
wsClients: new Set(),
sessionManager: {
get: vi.fn(async () => ({
sessionId,
status: "idle",
cwd: "/tmp/project",
workspaceRoot: "/tmp/project",
})),
// A restore that reuses the source id is what the hub does today.
restore: vi.fn(async () => ({
sessionId,
messages: restoredMessages,
checkpoint: { ref: "first", createdAt: 1, runCount: 1 },
})),
pendingPrompts: { list: vi.fn(async () => []) },
},
activeEnvironmentId: "local",
localWorkspaceRoot: "/tmp/project",
sessionEnvironmentIds: new Map([[sessionId, "local"]]),
runtimeBindings: new Map([
[
"local",
{
environmentId: "local",
kind: "local",
workspaceRoot: "/tmp/project",
sessionManager,
hubClient: { command: vi.fn(async () => undefined) },
unsubscribeSessionEvents: () => {},
},
],
]),
remoteEnvironments: null,
} as unknown as SidecarContext;
await handleChatSessionCommand(ctx, {
@@ -1,4 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
MAX_RECORDED_AUDIO_BASE64_BYTES,
MAX_RECORDED_AUDIO_BYTES,
@@ -30,6 +33,20 @@ function createTelemetryHandler(capture = vi.fn()) {
};
}
const originalSessionDataDir = process.env.CLINE_SESSION_DATA_DIR;
const temporaryDirectories: string[] = [];
afterEach(() => {
if (originalSessionDataDir === undefined) {
delete process.env.CLINE_SESSION_DATA_DIR;
} else {
process.env.CLINE_SESSION_DATA_DIR = originalSessionDataDir;
}
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});
describe("sidecar WebSocket payload limit", () => {
it("accepts every recording allowed by the voice input size limit", () => {
const handler = createWebSocketHandler({} as SidecarContext);
@@ -156,6 +173,41 @@ describe("sidecar HTTP origin checks", () => {
});
});
describe("session video artifacts", () => {
it("serves a generated video only from the session artifact directory", async () => {
const sessionsDir = mkdtempSync(join(tmpdir(), "desktop-video-artifact-"));
temporaryDirectories.push(sessionsDir);
process.env.CLINE_SESSION_DATA_DIR = sessionsDir;
const artifactsDir = join(sessionsDir, "session-1", "artifacts");
mkdirSync(artifactsDir, { recursive: true });
writeFileSync(join(artifactsDir, "video-result.mp4"), "video-bytes");
const response = await createHandler()(
new Request(
"http://127.0.0.1:3126/api/session-artifacts/session-1/video-result.mp4",
{ headers: { origin: "tauri://localhost" } },
),
createTestServer(),
);
expect(response?.status).toBe(200);
expect(response?.headers.get("content-type")).toBe("video/mp4");
await expect(response?.text()).resolves.toBe("video-bytes");
});
it("rejects untrusted origins for session artifacts", async () => {
const response = await createHandler()(
new Request(
"http://127.0.0.1:3126/api/session-artifacts/session-1/video.mp4",
{ headers: { origin: "https://attacker.example" } },
),
createTestServer(),
);
expect(response?.status).toBe(403);
});
});
describe("desktop error telemetry", () => {
it("captures sanitized webview error reports with structured context", async () => {
const server = createTestServer();
+132 -1
View File
@@ -1,5 +1,13 @@
import { randomUUID, timingSafeEqual } from "node:crypto";
import { captureSdkError } from "@cline/shared";
import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { basename, join } from "node:path";
import { Readable } from "node:stream";
import {
captureSdkError,
REALTIME_CLINE_TOOLS,
type RealtimeVoiceModeSession,
} from "@cline/shared";
import type { DesktopTransportRequest } from "../webview/lib/desktop-transport";
import { MAX_DESKTOP_TRANSPORT_PAYLOAD_BYTES } from "../webview/lib/voice-input-limits";
import { handleCommand } from "./commands";
@@ -12,6 +20,7 @@ import {
import { fetchMarketplaceCatalog } from "./marketplace";
import { cancelMcpOAuthAuthorizationsForOwner } from "./mcp-oauth";
import { cancelProviderOAuthLoginsForOwner } from "./oauth-login";
import { sharedSessionDataDir } from "./paths";
import {
BunRuntime,
SIDECAR_HOST,
@@ -62,6 +71,21 @@ function hasValidApprovalToken(url: URL, expectedToken: string): boolean {
);
}
function artifactContentType(filename: string): string {
const lower = filename.toLowerCase();
if (lower.endsWith(".mp3")) return "audio/mpeg";
if (lower.endsWith(".wav")) return "audio/wav";
if (lower.endsWith(".aac")) return "audio/aac";
if (lower.endsWith(".m4a")) return "audio/mp4";
if (lower.endsWith(".weba")) return "audio/webm";
if (lower.endsWith(".flac")) return "audio/flac";
if (lower.endsWith(".ogg")) return "audio/ogg";
if (filename.toLowerCase().endsWith(".webm")) return "video/webm";
if (filename.toLowerCase().endsWith(".mov")) return "video/quicktime";
if (filename.toLowerCase().endsWith(".mpeg")) return "video/mpeg";
return "video/mp4";
}
function readOrigin(req: Request): string | undefined {
const origin = req.headers.get("origin")?.trim();
return origin ? origin : undefined;
@@ -230,6 +254,73 @@ export function createFetchHandler(
);
}
if (
req.method === "GET" &&
url.pathname.startsWith("/api/session-artifacts/")
) {
if (!isTrustedRequestOrigin(req)) {
return new Response("Forbidden", { status: 403 });
}
const segments = url.pathname
.slice("/api/session-artifacts/".length)
.split("/")
.map((segment) => decodeURIComponent(segment));
const [sessionId, artifactName, ...extra] = segments;
if (
!sessionId ||
!artifactName ||
extra.length > 0 ||
sessionId === "." ||
sessionId === ".." ||
artifactName === "." ||
artifactName === ".." ||
basename(sessionId) !== sessionId ||
basename(artifactName) !== artifactName ||
!/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/.test(sessionId) ||
!/^[a-zA-Z0-9][a-zA-Z0-9._ -]*$/.test(artifactName)
) {
return new Response("Invalid artifact path", { status: 400 });
}
const artifactPath = join(
sharedSessionDataDir(),
sessionId,
"artifacts",
artifactName,
);
const artifactStat = await stat(artifactPath).catch(() => null);
if (!artifactStat?.isFile()) {
return new Response("Artifact not found", { status: 404 });
}
const range = req.headers.get("range")?.match(/^bytes=(\d+)-(\d*)$/);
const start = range ? Number(range[1]) : 0;
const requestedEnd = range?.[2]
? Number(range[2])
: artifactStat.size - 1;
const end = Math.min(requestedEnd, artifactStat.size - 1);
if (start < 0 || start > end || start >= artifactStat.size) {
return new Response(null, {
status: 416,
headers: { "content-range": `bytes */${artifactStat.size}` },
});
}
const body = Readable.toWeb(
createReadStream(artifactPath, { start, end }),
);
return new Response(body as unknown as BodyInit, {
status: range ? 206 : 200,
headers: {
...corsHeaders(req),
"accept-ranges": "bytes",
"cache-control": "private, max-age=31536000, immutable",
"content-length": String(end - start + 1),
"content-type": artifactContentType(artifactName),
...(range
? { "content-range": `bytes ${start}-${end}/${artifactStat.size}` }
: {}),
},
});
}
if (
url.pathname === "/transport" &&
isTrustedRequestOrigin(req) &&
@@ -246,6 +337,46 @@ export function createFetchHandler(
return undefined;
}
if (
url.pathname === "/api/modes/realtime/session" &&
req.method === "POST"
) {
if (!isTrustedRequestOrigin(req)) {
return createJsonResponse(
req,
{ error: "Untrusted request origin" },
403,
);
}
try {
const session = (await handleCommand(ctx, "create_mode_session", {
mode: "realtimeVoice",
})) as RealtimeVoiceModeSession;
if (session.kind !== "realtime") {
throw new Error("Realtime mode returned an unexpected session");
}
return createJsonResponse(req, {
token: session.token,
url: session.url,
...(session.expiresAt === undefined
? {}
: { expiresAt: session.expiresAt }),
tools: session.supportsTools ? REALTIME_CLINE_TOOLS : [],
});
} catch (error) {
return createJsonResponse(
req,
{
error:
error instanceof Error
? error.message
: "Failed to create realtime session",
},
400,
);
}
}
if (url.pathname === "/api/marketplace/catalog") {
try {
return createJsonResponse(req, await fetchMarketplaceCatalog());
@@ -48,6 +48,9 @@ export function discoverChatSessions(
const out: JsonRecord[] = [];
const store = new SqliteSessionStore();
for (const [sessionId, session] of ctx.liveSessions.entries()) {
if (session.config.executionTarget === "cloud") {
continue;
}
if (!session.busy && !session.prompt && session.messages.length === 0) {
continue;
}
@@ -292,6 +292,92 @@ describe("readSessionMessages", () => {
]);
});
it("projects generated video artifact blocks", async () => {
const sessionId = `video-projection-${Date.now()}`;
const liveSessions = new Map([
[
sessionId,
{
messages: [
{
id: "assistant-video",
role: "assistant",
content: [
{
type: "video",
mediaType: "video/mp4",
path: `/tmp/session/artifacts/video-result.mp4`,
},
],
},
],
},
],
]);
await expect(
readSessionMessages(
{ liveSessions } as Parameters<typeof readSessionMessages>[0],
sessionId,
),
).resolves.toEqual([
expect.objectContaining({
role: "assistant",
content: "",
videos: [
{
id: "assistant-video_video_0",
mediaType: "video/mp4",
artifactName: "video-result.mp4",
},
],
}),
]);
});
it("projects generated audio artifact blocks", async () => {
const sessionId = `audio-projection-${Date.now()}`;
const liveSessions = new Map([
[
sessionId,
{
messages: [
{
id: "assistant-audio",
role: "assistant",
content: [
{
type: "audio",
mediaType: "audio/mpeg",
path: "/tmp/session/artifacts/audio-result.mp3",
},
],
},
],
},
],
]);
await expect(
readSessionMessages(
{ liveSessions } as Parameters<typeof readSessionMessages>[0],
sessionId,
),
).resolves.toEqual([
expect.objectContaining({
role: "assistant",
content: "",
audios: [
{
id: "assistant-audio_audio_0",
mediaType: "audio/mpeg",
artifactName: "audio-result.mp3",
},
],
}),
]);
});
it("preserves absolute user run counts when older messages are omitted", async () => {
const sessionId = `run-count-projection-${Date.now()}`;
const liveSessions = new Map([
@@ -385,6 +471,66 @@ describe("readSessionMessages", () => {
]);
});
it("projects generated media nested in a tool result onto the tool message", async () => {
const sessionId = `tool-media-projection-${Date.now()}`;
const media = {
id: "generated-image-1",
modality: "image",
mediaType: "image/png",
source: { type: "base64", data: "aGVsbG8=" },
};
const liveSessions = new Map([
[
sessionId,
{
messages: [
{
role: "assistant",
content: [
{
type: "tool_use",
id: "generate-call",
name: "generate_media",
input: { media_type: "image", prompt: "A bee" },
},
],
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "generate-call",
name: "generate_media",
content: [
{ type: "text", text: "Generated an image." },
{ type: "media", media },
],
},
],
},
],
},
],
]);
const projected = (await readSessionMessages(
{ liveSessions } as unknown as Parameters<typeof readSessionMessages>[0],
sessionId,
)) as Array<Record<string, unknown>>;
expect(projected).toHaveLength(1);
expect(projected[0]).toMatchObject({
role: "tool",
media: [media],
meta: {
toolName: "generate_media",
hookEventName: "history_tool_result",
},
});
expect(String(projected[0]?.content)).not.toContain("aGVsbG8=");
});
it("preserves absolute run counts across system-displayed compaction messages", async () => {
const sessionId = `compaction-run-count-${Date.now()}`;
const liveSessions = new Map([
@@ -1,11 +1,12 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { basename, dirname } from "node:path";
import {
getUserRunSpan,
projectSessionMessagesForDisplay,
resolveMessageDisplayRole,
} from "@cline/core";
import {
type GeneratedMedia,
isGeneratedMedia,
type MessageWithMetadata,
validateImageMedia,
@@ -152,6 +153,35 @@ function extractImageBlock(
: undefined;
}
function projectGeneratedMediaFromToolResult(value: unknown): {
value: unknown;
media: GeneratedMedia[];
} {
const mediaById = new Map<string, GeneratedMedia>();
const visit = (nested: unknown): unknown => {
if (isGeneratedMedia(nested)) {
mediaById.set(nested.id, nested);
return `[generated ${nested.modality}]`;
}
if (Array.isArray(nested)) {
return nested.map(visit);
}
if (!nested || typeof nested !== "object") {
return nested;
}
const record = nested as JsonRecord;
if (record.type === "media" && isGeneratedMedia(record.media)) {
mediaById.set(record.media.id, record.media);
return `[generated ${record.media.modality}]`;
}
return Object.fromEntries(
Object.entries(record).map(([key, item]) => [key, visit(item)]),
);
};
return { value: visit(value), media: [...mediaById.values()] };
}
export function readPersistedChatMessages(
sessionId: string,
): MessageWithMetadata[] | null {
@@ -333,28 +363,37 @@ export async function readSessionMessages(
ctx: Pick<SidecarContext, "liveSessions">,
sessionId: string,
maxMessages = 800,
/** Explicit authoritative source for remote sessions; bypasses local disk. */
sourceMessages?: unknown[],
): Promise<unknown[]> {
const persisted =
readPersistedChatMessages(sessionId) ??
// A child agent's transcript is not stored under its own session
// directory — it lives beside the root session's artifacts — so opening a
// subagent session has to resolve the path recorded on its row.
readChildSessionMessages(sessionId);
const messages =
persisted && persisted.length > 0
const isRemoteRead = sourceMessages !== undefined;
const persisted = sourceMessages
? undefined
: (readPersistedChatMessages(sessionId) ??
// A child agent's transcript is not stored under its own session
// directory — it lives beside the root session's artifacts — so opening a
// subagent session has to resolve the path recorded on its row.
readChildSessionMessages(sessionId));
const messages = (sourceMessages ??
(persisted && persisted.length > 0
? persisted
: (ctx.liveSessions.get(sessionId)?.messages ?? []);
: (ctx.liveSessions.get(sessionId)?.messages ??
[]))) as MessageWithMetadata[];
const max = Math.max(1, maxMessages);
const start = Math.max(0, messages.length - max);
const displayMessages = projectSessionMessagesForDisplay(
messages.slice(start),
messages.slice(start) as MessageWithMetadata[],
).map((entry) => ({
message: entry.message,
sourceIndex: start + entry.sourceIndex,
}));
const baseTs = nowMs() - messages.length;
const out: JsonRecord[] = [];
const checkpointsByRunCount = readCheckpointEntriesByRunCount(sessionId);
// Remote artifacts belong to the SSH host. Never decorate them with a
// same-id local session's live transcript or checkpoint metadata.
const checkpointsByRunCount = isRemoteRead
? new Map<number, StoredCheckpointEntry>()
: readCheckpointEntriesByRunCount(sessionId);
const pendingToolMessages = new Map<string, [number, string, unknown]>();
let userRunCount = 0;
for (let idx = 0; idx < start; idx += 1) {
@@ -462,6 +501,16 @@ export async function readSessionMessages(
const textParts: string[] = [];
const images: Array<{ id: string; mediaType: string; data: string }> = [];
const videos: Array<{
id: string;
mediaType: string;
artifactName: string;
}> = [];
const audios: Array<{
id: string;
mediaType: string;
artifactName: string;
}> = [];
const reasoningParts: string[] = [];
let reasoningRedacted = false;
let textSegmentIndex = 0;
@@ -582,7 +631,10 @@ export async function readSessionMessages(
flushTextParts();
const toolUseId =
typeof record.tool_use_id === "string" ? record.tool_use_id : "";
const result = record.content ?? null;
const projectedResult = projectGeneratedMediaFromToolResult(
record.content ?? null,
);
const result = projectedResult.value;
const isError = Boolean(record.is_error);
const existing = pendingToolMessages.get(toolUseId);
if (existing) {
@@ -603,6 +655,9 @@ export async function readSessionMessages(
...(toolUseId ? { toolCallId: toolUseId } : {}),
hookEventName: "history_tool_result",
};
if (projectedResult.media.length > 0) {
target.media = projectedResult.media;
}
}
pendingToolMessages.delete(toolUseId);
} else {
@@ -611,6 +666,10 @@ export async function readSessionMessages(
sessionId,
role: "tool",
content: buildToolPayloadJson("tool_result", null, result, isError),
media:
projectedResult.media.length > 0
? projectedResult.media
: undefined,
createdAt: nextPartCreatedAt(),
meta: {
toolName: "tool_result",
@@ -643,6 +702,30 @@ export async function readSessionMessages(
}
continue;
}
if (blockType === "video") {
const mediaType = trimNonEmptyString(record.mediaType);
const path = trimNonEmptyString(record.path);
if (mediaType && path) {
videos.push({
id: `${messageIdBase}_video_${blockIdx}`,
mediaType,
artifactName: basename(path),
});
}
continue;
}
if (blockType === "audio") {
const mediaType = trimNonEmptyString(record.mediaType);
const path = trimNonEmptyString(record.path);
if (mediaType && path) {
audios.push({
id: `${messageIdBase}_audio_${blockIdx}`,
mediaType,
artifactName: basename(path),
});
}
continue;
}
if (blockType === "media" && isGeneratedMedia(record.media)) {
flushTextParts();
out.push({
@@ -683,6 +766,44 @@ export async function readSessionMessages(
textMeta = undefined;
}
}
if (videos.length > 0) {
const target = out
.slice(outStartIndex)
.find((item) => item.role === role);
if (target) {
target.videos = videos;
} else {
out.push({
id: `${messageIdBase}_videos`,
sessionId,
role,
content: "",
videos,
createdAt: nextPartCreatedAt(),
meta: textMeta,
});
textMeta = undefined;
}
}
if (audios.length > 0) {
const target = out
.slice(outStartIndex)
.find((item) => item.role === role);
if (target) {
target.audios = audios;
} else {
out.push({
id: `${messageIdBase}_audios`,
sessionId,
role,
content: "",
audios,
createdAt: nextPartCreatedAt(),
meta: textMeta,
});
textMeta = undefined;
}
}
flushReasoningParts();
if (textMeta && out[outStartIndex]) {
out[outStartIndex].meta = {
@@ -2,13 +2,13 @@ import { getFileIndex } from "@cline/core";
import type { SidecarContext } from "../types";
export function searchWorkspaceFiles(
ctx: Pick<SidecarContext, "workspaceRoot">,
ctx: Pick<SidecarContext, "localWorkspaceRoot">,
args?: Record<string, unknown>,
): Promise<string[]> {
const root =
typeof args?.workspaceRoot === "string" && args.workspaceRoot.trim()
? args.workspaceRoot.trim()
: ctx.workspaceRoot;
: ctx.localWorkspaceRoot;
const query =
typeof args?.query === "string" ? args.query.trim().toLowerCase() : "";
const limit =
+39 -19
View File
@@ -8,7 +8,12 @@ import type {
ToolApprovalResult,
} from "@cline/core";
import type { MessageWithMetadata } from "@cline/llms";
import type { UserContext } from "@cline/shared";
import type {
RemoteEnvironmentConnection,
RemoteEnvironmentService,
} from "./remote-environments";
export const LOCAL_ENVIRONMENT_ID = "local";
export type JsonRecord = Record<string, unknown>;
@@ -22,6 +27,8 @@ export type ChatSessionCommandRequest = {
| "start"
| "attach"
| "send"
| "prepare_handoff"
| "handoff"
| "stop"
| "abort"
| "fork"
@@ -37,8 +44,13 @@ export type ChatSessionCommandRequest = {
checkpointRunCount?: number;
forkBeforeRunCount?: number;
delivery?: "queue" | "steer";
source?: "desktop" | "realtime";
config?: JsonRecord;
attachments?: ChatTurnAttachments;
/** Opaque preflight result returned by prepare_handoff and revalidated by handoff. */
fingerprint?: JsonRecord;
/** Optional first prompt to queue after ownership moves to the cloud session. */
nextCommand?: string;
};
export type PromptInQueue = {
@@ -50,6 +62,7 @@ export type PromptInQueue = {
};
export type LiveSession = {
environmentId?: string;
config: JsonRecord;
messages: MessageWithMetadata[];
promptsInQueue: PromptInQueue[];
@@ -61,11 +74,6 @@ export type LiveSession = {
prompt?: string;
title?: string;
attachedViaHub?: boolean;
/** Iterations already in flight when the user supplied recovery guidance. */
mistakeRecovery?: {
latestIteration: number;
continuedThroughIteration?: number;
};
/** Materialized attachment files for prompts still waiting in the queue. */
queuedAttachmentFiles?: Map<string, string[]>;
/** Last prompt id announced via chat_queued_prompt_start, to dedupe emits. */
@@ -74,6 +82,16 @@ export type LiveSession = {
consumedAttachmentFiles?: Map<string, string[]>;
};
export type SessionRuntimeBinding = {
environmentId: string;
kind: "local" | "ssh";
workspaceRoot: string;
sessionManager: ClineCore;
hubClient: NodeHubClient;
unsubscribeSessionEvents: () => void;
remote?: RemoteEnvironmentConnection;
};
export type ToolApprovalRequestItem = {
requestId: string;
sessionId: string;
@@ -88,8 +106,12 @@ export type ToolApprovalRequestItem = {
export type PendingToolApproval = {
item: ToolApprovalRequestItem;
owner: SidecarWebSocketClient;
resolve: (result: ToolApprovalResult) => void;
// Approvals created for a trusted desktop connection carry that owner and
// may only be listed/answered by it. Cloud-session approvals are relayed
// from a pod without a local owner and stay answerable from any trusted
// surface (and survive local disconnects).
owner?: SidecarWebSocketClient;
resolve: (result: ToolApprovalResult) => void | Promise<void>;
};
export type AskQuestionRequestItem = {
@@ -121,23 +143,21 @@ export type SidecarContext = {
liveSessions: Map<string, LiveSession>;
restoringWorkspacePaths: Set<string>;
streamIndices: Map<string, number>;
/**
* Identifies this sidecar process. `streamIndices` restarts whenever the
* sidecar does, so the webview needs to tell "index 1 of a new process"
* apart from a replay of the run it already rendered.
*/
bootId: string;
wsClients: Set<SidecarWebSocketClient>;
pendingApprovals: Map<string, PendingToolApproval>;
pendingQuestions: Map<string, PendingAskQuestion>;
sessionManager: ClineCore | null;
hubClient: NodeHubClient | null;
workspaceRoot: string;
runtimeBindings: Map<string, SessionRuntimeBinding>;
sessionEnvironmentIds: Map<string, string>;
activeEnvironmentId: string;
remoteEnvironments: RemoteEnvironmentService | null;
localWorkspaceRoot: string;
logger?: BasicLogger;
telemetry?: ITelemetryService;
/** Analytics identity and explicit account state forwarded with each session. */
telemetryUser?: UserContext;
unsubscribeSessionEvents: (() => void) | null;
cloudSessionManager: {
dispose(): Promise<void>;
isCloudSession(sessionId: string): boolean;
} | null;
/**
* Latest managed Hub build mismatch, broadcast as `hub_build_mismatch` and
* replayed to webviews that connect after the event fired.
+1
View File
@@ -519,6 +519,7 @@ dependencies = [
name = "cline-app"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"notify-rust",
"objc2",
"objc2-app-kit",
@@ -7,9 +7,10 @@ edition = "2021"
tauri-build = { version = "2.0.0", features = [] }
[dependencies]
base64 = "0.22"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "2.11.1", features = ["image-png", "tray-icon"] }
tauri = { version = "2.11.1", features = ["image-png", "macos-private-api", "tray-icon"] }
notify-rust = "4.18"
tauri-plugin-notification = "2"
tauri-plugin-updater = "2"
@@ -2,13 +2,9 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-window",
"description": "Permissions required by the main desktop window.",
"windows": ["main"],
"windows": ["main", "avatar-overlay"],
"permissions": [
"core:default",
"core:window:allow-close",
"core:window:allow-is-maximized",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-set-title",
"core:window:allow-start-dragging",
"notification:default"

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Some files were not shown because too many files have changed in this diff Show More