The standalone plugin test compared the child's process.execPath to
realpathSync(cli), which keeps Windows 8.3 short names (RUNNER~1) while the
child reports the long path. Use realpathSync.native. The dashboard test's
cleanup could hit EBUSY while the stopped Hub released its cwd; retry it like
the startup scenario cleanup does.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
The desktop backend now runs as `BUN_BE_BUN=1 cline-cli run <bundle>`.
In that mode Bun ignores the flags baked into the CLI with
--compile-exec-argv, so the backend lost the --use-system-ca that #14462
added and went back to trusting only Bun's bundled roots on macOS and
Windows. Requests the backend makes itself (model lists from a LiteLLM or
other intranet endpoint, sign-in refresh, marketplace, cloud sessions)
failed with "unable to get local issuer certificate" behind a corporate
CA. Pass the flag on the run command, and mirror it in the startup test.
* feat(desktop): enable cloud sessions for everyone
Remove the Cloud sessions preview toggle from Settings and the
code-cloud-agents rollout flag that revealed it. Cloud sessions are now
on by default; CLINE_CODE_CLOUD_AGENTS=0 still turns them off for
development and tests.
* fix(desktop): keep the code-cloud-agents rollout flag as the cloud gate
Only the Settings opt-in should go away. Cloud sessions are enabled
wherever the code-cloud-agents PostHog flag is on (or the
CLINE_CODE_CLOUD_AGENTS env override forces them).
* Let hosts choose the hub daemon launcher and defer spawn-only setup
Adds setHubDaemonLauncher so an embedding process can start the hub
daemon through a bundled Cline CLI instead of its own executable, and a
beforeSpawn hook on ensureDetachedHubServer that only runs when a new
daemon is actually started.
* Add programmatic options to cline hub ensure
hub ensure/start gain --json (URL + auth token), --allow-port-fallback,
--no-connectors and --login-shell-path, and the hub command accepts
--discovery-path for a dedicated discovery record. These let the desktop
app and SSH remotes start or reuse a hub through the CLI.
Also extracts the standalone binary compile step into
script/compile-binary.ts so other packages can build the CLI binary.
* Run SSH remote hubs through the Cline CLI
SSH remotes now upload the Cline CLI binary and start or reuse the remote
hub with 'cline hub ensure --json --discovery-path ...', stopping it with
'cline hub stop'. The dedicated remote helper entrypoint is removed;
cleanup records from older releases still stop through the old flag.
* Run the desktop backend on the bundled Cline CLI
The desktop app no longer compiles its own executable. It bundles the
self-contained Cline CLI (externalBin cline-cli) plus the backend as a JS
bundle that runs on the CLI's embedded runtime. At startup the backend has
the CLI run 'hub ensure', which reuses a compatible healthy Hub or starts
one, and any Hub it starts later also goes through the CLI binary. Linux
CLI copies are bundled for SSH remotes.
* Launch the desktop backend through the bundled Cline CLI
The Tauri shell resolves the bundled cline-cli and the backend bundle
(per-platform resource layouts) and starts 'cline-cli run' on it. The
Windows installer stops users of cline-cli.exe, and also stops and removes
the backend executable of earlier releases on upgrade.
* Recognize the CLI-hosted desktop backend in cline doctor --fix
* Update desktop publish workflow and docs for the bundled Cline CLI
Telemetry selfchecks and smoke tests run the backend bundle on the
bundled CLI, and the Windows signature gate verifies cline-cli.exe.
* fix(desktop): address runtime packaging review findings
* fix(desktop): preserve failed hub cleanup and handle exited daemons
* fix(hub): distinguish missing discovery records from read failures
* fix(hub): preserve discovery recovery while enforcing strict shutdown reads
* fix(cli): do not count hub commands as CLI activations (#14870)
* fix(cli): do not count hub commands as CLI activations
The desktop app runs `cline-cli hub ensure` on every launch and the SSH
remote flow runs `cline hub ensure` on every connect. Both went through
runCli(), which captured user.extension_activated unconditionally, so each
desktop launch was double-counted as a CLI activation and every SSH remote
host (with its own distinct id) showed up as a new CLI user.
Skip the activation capture for the hub subcommand; it is plumbing other
clients invoke, not a user activation. Hub-owned task telemetry is
unaffected since the daemon path never enters runCli().
* test(cli): clear the hub command mock between main.test.ts cases
* test(cli): stop stubbing the hub command module in main.test.ts
The module-wide vi.mock("./commands/hub") replaced createHubCommand with
a no-op, so `cline hub dashboard` (routed through createHubCommand) never
reached the dashboard mock and three dashboard tests failed. Drop the
module mock and assert the activation exclusion through
`hub dashboard --no-open`, which is already mocked.
---------
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
* fix(desktop): ship the SDK packages plugins import in production builds
Plugin installs strip @cline/* dependencies because the host provides them,
and the plugin worker resolves those from node_modules beside its bootstrap.
The packaged desktop app ships only the bootstrap, so a marketplace plugin
like goal failed with "Cannot find module '@cline/sdk'" (dev builds worked
because the repo's node_modules sit above src-tauri).
Bundle self-contained @cline/sdk, core, agents, llms and shared next to the
bootstrap (one shared copy via code splitting, ~17 MB), and widen the Tauri
resource glob so the nested files are actually bundled.
* fix(desktop): ship the SDK packages' subpath exports for plugins
Plugins can import supported subpaths such as @cline/shared/storage or
@cline/core/telemetry. Bundle every runtime export of each package and write
its exports map, skipping type-only exports that have no JS.
* feat(core): load plugins in the hub process and surface plugin errors
Plugins now load in-process through one registry per process instead of a
node/bun subprocess. The compiled CLI and the hub it starts no longer need a
runtime on PATH, and plugins can use the Bun global under the Bun-built hub.
The subprocess sandbox stays available with CLINE_PLUGIN_MODE=sandbox.
The registry wraps import, setup, every hook, tool, and command call with
timeouts and error capture, and tracks loading/ready/degraded/failed/disabled
per plugin with the last error's phase, message, stack, and counts. A failing
plugin no longer silently drops its tools:
- session.create takes a `plugins` policy shaped like toolPolicies, and its
reply and session.created event carry `pluginIssues`
- failures after start (setup and hooks run on the first turn) reach each
affected session as a status notice
- the hub answers plugins.list/status/reload and broadcasts
plugin.status_changed; `cline hub plugins [--reload]` wraps them
- the daemon marks a plugin failed instead of shutting down when an uncaught
error's stack points into it
- the CLI prints a warning naming the plugin and error, and
`cline config tools --json` lists failed plugins as plugin_failure entries
Setup and tool contexts gain cwd and emitEvent; __clinePluginHost keeps
working as a shim. Compiled binaries fall back to jiti/static when babel is
not on disk next to them.
* fix(core): give each session its own copy of every plugin module
Plugins written for the per-session sandbox keep state at module level. The
cline-monitor plugin, for example, builds one plugin object per import and
throws "Create a separate monitor plugin handle for each session" on a second
setup(). Sharing one imported copy across hub sessions broke it on the
second session.
The registry now hands each session its own copy: the first session claims
the copy imported at startup, and later ones re-evaluate the module while
reusing jiti's transform cache (about 180ms for cline-monitor instead of
2.8s with no cache).
A setup() failure now costs only the session it happened in: other sessions
keep their copies, the next session tries again, and a successful setup
returns the status to ready. Import failures, attributed uncaught errors, and
repeated hook/tool failures still turn the plugin off for every session.
* docs(sdk): document the plugin hooks that exist and how hook failures behave
The Hook Stages list (snake_case stages such as tool_call_before) and the
Hook Policies table (mode, retries, queueLimit, maxConcurrency) described no
code: plugin hooks are the seven runtime hooks, and there is no per-hook
policy object. Replace both with the real hooks, what each can return, and
how hookErrorMode and the 3-second hook timeout apply.
* fix(core): report failed plugins as failed in settings
Settings marked a plugin "failed" only when the plugin tool report had no
entry for it. The report now lists failed plugins with their state, so they
showed as "available" (desktop Extensions page and other core settings
hosts). Use the reported state, and fill loadError with the failing phase
and message so the reason is visible.
Also tell only the calling session about a per-session setup failure;
other sessions' copies still work. Keep the hub log line's own message
instead of letting the plugin error text overwrite it.
* fix(core): address plugin registry review findings
Reload and generations: each session copy records the generation (import)
it came from. A reload starts a new generation without invalidating older
copies, so running sessions keep working. Failures and blocking are counted
per generation, so a broken reload cannot turn off copies that still work,
and an old copy's failures cannot change the current status. A stray error
attributed by stack turns off every live generation, since a stack cannot
tell copies of one file apart.
Sandbox mode: the hub daemon no longer preloads plugins in sandbox mode,
and tool listing and MCP settings sync inspect plugins through the sandbox.
The session plugin policy now applies in sandbox mode: paths whose file
name the policy turns off never reach the sandbox, and plugins matched only
by their exported name are dropped after import.
Calls: commands, rule content, and message builders go through the same
guarded path as hooks and tools (timeout, error capture, failure count), and
calls into a copy that is turned off, failed setup, or whose session ended
are refused. A failing rule contributes no text and a failing message
builder leaves messages unchanged.
Cleanup: ctx.onDispose registers per-session cleanup. Timers a copy creates
while its own code runs, including at import, are tracked and cleared when
the session releases the copy; host callbacks given to plugins run outside
that scope so host timers are never cleared with them. Release runs on
session stop and dispose, and now also when building the session runtime
fails. The automation-events example uses ctx.onDispose.
Registrations: automation event types are validated while setup runs. If
the host still rejects a registration on commit, the copy's setup is
recorded as failed and the error is rethrown so the partial registrations
are discarded.
Also: forward importTimeoutMs, hookTimeoutMs, and contributionTimeoutMs to
the in-process registry per session; key the plugin tool inspection cache on
registry status; keep the module copy imported by preload for the first
session; report settings-disabled plugins a session would use as issues
with reason "settings".
* fix(core): keep this bindings and route import-time plugin events
- Timer tracking forwards the runtime's this (the timer handle in Node and
Bun) to plugin callbacks.
- Plugin methods keep their receiver: rule content, message builders,
command handlers, and setup() are called on their own objects, including
setup() in the existing in-process loader.
- Each plugin copy owns one async scope from import onward. Claiming the
copy for a session fills in that scope's session id and event callback,
so events emitted by work the module started at import reach the session.
- Each generation tracks its own health, so an older copy's first failure
after a reload is reported to its sessions even though the status shows
the reloaded generation.
* fix(core): bind plugin changes to sessions started afterwards
Plugins are bound to a session when it starts; added, edited, or reloaded
plugins reach only sessions started afterwards. Change detection used the
entry file's mtime and size, which missed edits to files the entry imports
(common for package plugins) and same-size edits within one clock tick, so
the next new session could claim a stale preloaded copy.
Detect changes with a content fingerprint of the entry file and every file
it reaches through relative static imports, for the registry, the plugin
tool inspection cache, and the plugin command service. Add a bootstrap test
that a session started before a plugin is added or changed keeps its
original tools even when its setup runs afterwards, and a registry test for
a change outside the entry file.
* fix(core): include side-effect imports in plugin change fingerprints
The fingerprint only followed `from`, `import()`, and `require()`
specifiers, so a fix to a file the entry imports only for side effects
(`import "./setup.js"`) left the fingerprint unchanged and new sessions kept
getting the failed entry.
Scan statement-level side-effect imports for the fingerprint only. They are
kept out of the dependency preflight, where a loose match on text inside a
string (`'run import "x" first'`) would reject a valid plugin.
* fix(core): retry failed plugin imports on a backoff
Change detection follows static and side-effect relative imports, but a
broken import can also be fixed where no fingerprint sees it: a dynamically
imported file, a file behind a block comment, or a dependency installed in
node_modules. Such plugins stayed failed until an explicit reload.
Retry a plugin whose import failed when a session starts, at most every 30
seconds, even if no change was detected. A broken plugin costs at most one
extra import attempt per 30 seconds, bounded by the import timeout.
Replace the SHA-1 fingerprint with a non-cryptographic checksum; it only has
to notice edits to plugin source.
* feat(desktop): show plugin failures in the chat
Plugin failures reached the desktop webview only as core logs written to the
devtools console. Show them as status lines in the chat:
- session start returns `pluginWarnings` for plugins the session asked for
but could not load, and the webview adds one status line per warning
- plugin notices that arrive later on the session stream (setup and hooks
run on the first turn) carry `metadata.pluginIssue`; the webview adds
those as status lines too, while other core logs stay out of the
transcript
* fix(desktop): keep plugin warnings when saved history reloads
Finishing a turn replaces the live transcript with saved history, which
never contains plugin warnings (they come from the start reply and stream
notices), so the explanation of a missing plugin disappeared after the first
turn. Carry the session's live plugin-warning lines into canonical history
at their original position by time.
* refactor(core): simplify the in-process plugin registry (#14999)
* refactor(core): simplify the plugin registry
Drop the pieces added in review rounds that cost more than they buy:
- Global timer tracking. It monkeypatched setTimeout/setInterval/
setImmediate and their clear* counterparts on globalThis for the whole
host process (the hub, the CLI, and the shared VS Code extension host),
and attributed timers by async scope, so a host callback invoked from
inside a plugin call had its timers cancelled when the session released
its plugin copy. Plugins stop what they start in ctx.onDispose, which
stays.
- Plugin generations and per-generation health. Blocking and consecutive
failures are counted per plugin again; a re-import (reload or source
change) resets them. Running sessions keep their copies callable unless
the plugin is turned off, so a broken reload fails closed for them.
- Automatic retry of failed imports on a 30 s backoff, and the side-effect
import scan in the change fingerprint. plugins.reload covers changes the
fingerprint cannot see.
- The duplicated registerAutomationEventType validation; the commit
try/catch already records a host-rejected registration as a setup
failure.
* refactor(core): do not apply the session plugin policy in sandbox mode
The sandbox is an opt-in escape hatch and the per-session policy is new;
filtering sandbox results by file-derived and exported names added two
passes nobody needs yet. The policy is documented as in-process only.
* revert(desktop): drop the live plugin-warning merge into saved history
Reverts 90df99574. The merge re-inserted unpersisted warning lines by
timestamp after every history reload and still lost them on reopen.
* fix(core): keep plugin status consistent across a reload and a late block
- A re-import now resets `blocked` and the failure count only after it
succeeds, so calls that hit the failure threshold while the import was in
flight cannot leave the plugin blocked under a `ready` status.
- Sessions are notified when a plugin is turned off even if its status
already read `failed` from another session's setup failure.
- Docs: say which relative imports the change fingerprint follows, that a
failed reload turns the plugin off for running sessions too, and that the
sandbox ignores the session plugin policy.
* fix(core): attribute stray plugin errors by the plugin's real path too
Module resolution follows symlinks and, on Windows, expands 8.3 short names
(the GitHub runner's temp dir is C:\Users\RUNNER~1\...), so a stack frame can
spell the plugin path differently from the configured one and the uncaught
error was never attributed. Match the realpath as well, and compare with
normalized separators and case on win32. Fixes the Windows-only failure of
"attributes a stray error to the plugin whose file is in its stack", which
also fails on the base branch.
* fix(core): match both realpath spellings when attributing plugin errors
On Windows fs.realpathSync keeps 8.3 short names while realpathSync.native
expands them, and module resolvers differ in which they use. Accept either
so the stack frame matches regardless.
* fix(core): do not count a stale plugin copy's failures against the reloaded module
Each copy is stamped with the entry's import counter. After a reload (or a
source change), failures from copies of the previous import are reported to
their own session but no longer bump the error counts, block the plugin, or
change its status, so a session still running a broken copy cannot turn off
the fixed plugin for everyone else. The pre-review registry had this guard
via its generation number; the simplification dropped it.
* fix(core): don't let a stale session import turn off a reloaded plugin
A session's copy import that failed after plugins.reload (or a source change)
had already re-imported the plugin was recorded against the current module:
the catch around claimInstance had no import identity, so recordFailure
blocked the plugin and dropped the freshly loaded copy for every session.
Capture the entry's importId before claiming. If it changed by the time the
import fails, report the failure to that session only and leave the current
module's status alone, matching how stale copies' call failures are handled.
* fix(desktop): don't duplicate a queued prompt behind a plugin start warning
Session start appends plugin warnings after the optimistic user bubble. The
chat_queued_prompt_start handler searched backward for that bubble and
stopped at the first non-user row, so it hit the warning, missed the bubble,
and appended the prompt a second time until saved history replaced it.
Skip plugin warning rows in that search.
* feat(core): default a worker-thread transport for isolated plugins (#15034)
* feat(core): add a worker-thread transport for isolated plugins
CLINE_PLUGIN_MODE=worker runs each session's plugins in a worker thread
inside the host runtime. It reuses the subprocess sandbox protocol and
bootstrap, so it needs no node/bun executable on PATH and works from the
compiled CLI and hub. terminate() stops a plugin stuck in a synchronous
loop, and an uncaught error or process.exit() ends only that worker.
- WorkerSandbox mirrors SubprocessSandbox (timeouts, idle reclaim,
per-worker pending calls) over the worker message port.
- The bootstrap talks to parentPort in a worker and posts ready once it
listens; the host holds calls until then because Bun drops earlier
messages.
- Workers override process.cwd() instead of chdir, which Bun would apply
to the whole host process.
- Uncloneable values fall back to JSON, matching the subprocess IPC.
- The sandbox test suite runs against both transports, plus worker-only
tests for loops, stray throws, process.exit, cwd, and no runtime.
* fix(core): ship a self-contained plugin bootstrap and snapshot queued worker calls
The CLI copies plugin-sandbox-bootstrap.js beside the compiled binary,
where nothing can resolve @cline/shared or jiti, so a worker (or
subprocess) exited before initializing any plugin. Bundle every package
into the bootstrap and fail the core build if it imports anything but
Node builtins.
Calls made before a worker reports ready kept the caller's args object,
so later changes leaked into the call and an uncloneable value added in
that window threw from the message listener. Queue a structured clone
instead, skip calls that already timed out, and reject a call whose
flush fails.
* test(core): give the worker loop-recovery test room for a cold worker on CI
The recovery call boots a fresh worker and re-imports the plugin within
the same tool timeout, which 200ms did not cover on CI runners.
* fix(core): keep worker plugin stdout off the host's stdout under Bun
Bun workers write console.log and process.stdout straight to the host's
stdout (Bun ignores the Worker stdout option), so a plugin's logging
corrupted --json output and JSON-RPC streams. Discard stdout-bound output
inside the worker, matching the subprocess sandbox's stdio "ignore".
* fix(core): complete discarded stdout write callbacks in plugin workers
The worker's process.stdout.write replacement dropped the completion
callback, so a plugin awaiting a write hung until the call timed out.
Call it on the next tick for both write(chunk, cb) and
write(chunk, encoding, cb).
* feat(core): run plugins in workers by default, through the plugin registry
Worker mode now goes through PluginRegistry instead of around it, so it
keeps everything in-process mode has: plugin status and plugins.list/
status/reload, plugin.status_changed, degraded/failed tracking with the
failure threshold, session notices, the per-session plugins policy,
stale-copy protection, and per-session timeouts. Only where a copy runs
changes: each plugin gets its own worker per session, and the copy the
registry wraps is a proxy to it.
One worker per plugin means a worker that dies on its own (uncaught
error, unhandled rejection, process.exit) is that plugin's failure with
no stack attribution: the plugin is turned off and the host and other
plugins keep running. The hub preloads status in throwaway workers.
- CLINE_PLUGIN_MODE defaults to worker; in_process and sandbox are
opt-ins. Hosts that ship no bootstrap fall back to in_process.
- WorkerSandbox reports unexpected exits; sandbox tool proxies honor a
tool's own timeoutMs.
- The bootstrap is resolved lazily and can be set with
CLINE_PLUGIN_BOOTSTRAP_PATH. The desktop app ships it as a Tauri
resource and points the sidecar at it; the VS Code build copies it
next to the bundle as .mjs.
* fix(core): carry setup context, tool context, and tool definitions into plugin workers
Worker plugins lost what in-process plugins get, now that workers are the
default:
- Setup context: ctx.cwd, ctx.emitEvent, and ctx.onDispose. Cleanup runs
in the worker before it stops (session release, shutdown, idle
reclaim); calls arriving during an idle stop wait for a fresh worker.
- Tool context: the abort signal (the host forwards aborts by call id),
emitUpdate (routed back to the calling tool), and emitEvent.
- Tool definitions: lifecycle, executionMode, resultPolicy, maxRetries,
and any other plain field now reach the runtime.
- Setup gets its own timeout inside the worker. A setup that throws,
times out, or takes the worker down fails only that session's copy,
instead of being reported as an import failure that turns the plugin
off for every session.
* fix(core): don't treat an exit during plugin cleanup as a crash
stopGracefully() ran the dispose method before marking the worker as
stopping, so a disposer that called process.exit() or threw from a timer
looked like an unexpected exit and turned the plugin off for every
session. Mark the worker as stopping before cleanup starts.
* fix(core): apply the session plugin policy by a worker plugin's own name
The host checked the policy against the file-derived name before the
worker had imported the plugin, so a policy keyed by the plugin's
exported name misfired: "*" off plus the plugin turned on by name
skipped it for good, and turning it off by name still ran its setup().
The worker now applies the policy after import, against both names, and
before setup. The host skips the worker up front only once it has seen
the plugin's own name for its current code.
* fix(core): keep setup prompts and cleanup for worker plugins
Worker plugins run setup() while the session bootstraps, before it is
registered, unlike in-process plugins whose setup runs on the first turn.
- Prompts a plugin sends to its session during setup (steer_message,
queue_message, pending_prompt) found no session and were dropped. The
host now holds them until the session is registered, then delivers them.
- A setup that registered ctx.onDispose and then threw or timed out lost
its cleanup, since only plugins that loaded are disposed later. The
worker now runs that cleanup right away, so a child process started
before the failure does not outlive it.
---------
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
* fix(schedules): claim queued runs under bun:sqlite
claimDueRuns bound unprefixed named parameters, which bun:sqlite binds
as NULL, so the desktop hub never claimed a queued run and every
scheduled run stayed pending with no error.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(schedules): fail runs whose agent turn ends with an error
Provider failures such as a retired model id resolve the turn with
finishReason 'error' instead of throwing, so these runs were recorded
as successful with no error message.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(desktop): load live models in the schedule editor
The editor only used the bundled provider catalog, so it kept offering
retired models (e.g. space-bunny) that the live list no longer has.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(schedules): skip superseded queued cron occurrences
Once queued runs are claimable again, the backlog that piled up while
claiming was broken would replay every missed slot back to back. Cancel
queued cron occurrences that a newer queued occurrence of the same
schedule supersedes so only the latest runs.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(schedules): keep usage and tool calls in error-finish run reports
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* test(schedules): cover claimDueRuns under bun:sqlite
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* feat(desktop): add transcript outline with shared message scroller
* fix(desktop): avoid repeated outline work during streaming
* fix(ui): preserve transcript position when replacing rows
* fix(ui): respect reduced motion in MessageScrollerButton
The shared scroll-to-end button always passed shadcn's default smooth
behavior. Resolve it from prefers-reduced-motion like the outline does,
keeping an explicit behavior prop as an override.
* test(desktop): cover saved-session row swap and streaming growth
Add a regression test for continuing a saved session: history hydrates
after the provider mounts, a new prompt pins near the top, then the
activity row is swapped for the first answer row in one commit and the
viewport must stay on the new prompt. Make the streaming test grow the
transcript and deliver the resize notification instead of only changing
answer text.
* chore(ui): tidy message-scroller check in smoke-package
---------
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
* fix(desktop): sync session titles changed on the web
* fix(desktop): preserve unopened session renames during discovery
* fix(desktop): keep title refreshes ahead of React commits
* fix(desktop): limit refreshed title reconciliation to cloud sessions
* fix(desktop): keep every renamed title ahead of refresh completion
* refactor(desktop): reuse session history lookup for chat header
Drop the fixed max width on the composer's model picker so long model
names show in full when the settings row has room, truncating only when
it runs out. Keep the settings row on one line instead of wrapping,
reserve room for the model and thinking controls so a long branch name
can't squeeze the model name out, and give the conversation layout a
shrinkable grid column so the composer row truncates instead of being
clipped at narrow widths.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
get_git_branch only needs the current branch, but it ran listGitBranches,
which also executes git for-each-ref. Over an SSH environment each command is
its own ssh login, so the 5s branch label poll opened two fresh SSH sessions
every 5 seconds against the remote host for as long as it was connected.
Run only git branch --show-current for the label, and poll it every 30s for
SSH environments (5s stays for local). Branch switches made through the picker
still refresh immediately, as do focus and visibility changes.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(desktop): stop UPX-packing the Linux SSH remote helpers
Since the Bun 1.4.2 upgrade (#14631) the compiled helper lazily reads its
embedded module payload back from its own executable at runtime. On a
UPX-packed binary those reads return NUL padding, so the helper starts but
every --remote-hub-ensure fails with "SyntaxError: Invalid character:
'\0'" and SSH remotes cannot start (desktop 0.0.38 through 0.0.44).
Ship the helpers uncompressed again and drop the upx installs from the
publish workflow. Bundles grow by roughly 175 MB until the CLI-managed Hub
replaces the helper binary.
Fixes#14815Fixes#14781
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* ci(desktop): smoke-test the bundled SSH remote helper before publishing
Run each Linux job's native remote helper through --remote-hub-ensure and
--remote-hub-stop against an isolated CLINE_DIR and discovery record. The
UPX regression started the helper fine but failed on exactly this path,
and nothing in the publish workflow exercised it.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
`ai` had stayed at 7.0.79 while single `@ai-sdk/*` packages were bumped on
their own. Each such bump brought its own `@ai-sdk/provider` /
`provider-utils`, whose types no longer unify with the ones `ai` pins, so
the hand-off to `ai` needed casts. Bumping `@ai-sdk/openai` alone for #14649
would have added two more.
Move `ai` and every `@ai-sdk/*` package to the releases built on
`@ai-sdk/provider-utils` 5.0.50 (all published 2026-09-28): ai 7.0.120,
gateway 4.0.98, anthropic 4.0.67, google 4.0.84, google-vertex 5.0.97,
mistral 4.0.53, openai 4.0.79, openai-compatible 3.0.58, otel 1.0.120,
react 4.0.123, provider 4.0.19. Bedrock 5.0.98 is already on this line and
stays. One copy of each is left in the lockfile; the only other `ai` is
Langfuse's own nested copy, as before. No casts are added.
This also fixes#14649: `@ai-sdk/openai` 4.0.79 knows which GPT-6 models
can turn reasoning off and drops the setting for the rest, so commit
message generation (which always turns reasoning off) and tasks with
reasoning set to "none" stop getting a 400 from GPT-6 Astra and 6.1 Sol.
GPT-5.x and GPT-6 Sol/Luna still get "none".
It fixes#13203 the same way: `@ai-sdk/anthropic` 4.0.67 leaves
`thinking` out for Claude models that can't turn thinking off (Fable 5,
Opus 5.5) instead of sending `thinking: {type: "disabled"}`, which they
reject. Other Claude models still get `disabled`.
* fix(desktop): let keyless configured providers start sessions
The webview's pre-flight credential gate demanded an API key for every
provider that was not OAuth or local-auth, so a local OpenAI-compatible
endpoint (LM Studio, vLLM, Ollama) migrated from VS Code or added via
Settings failed every turn with "Missing API key" even though the
sidecar already reported it as configured via base URL + model.
Carry the catalog's host-computed `configured` readiness into the chat
config and skip the API-key requirement when the host accepts the
provider, matching the runtime's own keyless handling.
* fix(desktop): gate sessions on the shared apiKeyOptional provider fact
Address review: instead of a desktop-only providerConfigured readiness
flag threaded through the chat config, make the credential requirement
explicit in the shared provider contract.
- @cline/shared: GatewayProviderMetadata.apiKeyOptional, mirrored into
ProviderAuthInfo via resolveProviderApiKeyOptional (same pattern as
localCliCommand -> localCli).
- @cline/llms: declare it on openai-compatible, ollama, lmstudio, litellm,
vertex, and bedrock.
- @cline/core: listLocalProviders carries the fact in ProviderListItem.auth;
user-added endpoints registered from providers.json / models.json declare
it too (their key is already optional in the desktop form).
- desktop: resolveCredentialError consults auth.apiKeyOptional and no
longer needs the extra config field.
* feat(desktop): render Mermaid diagrams in chat
Render model-authored Mermaid fences as interactive SVGs in Desktop without depending on model image-input support.
- extract the strict lazy Mermaid DiagramPlugin into @cline/ui
- keep shared defaults diagram-free and add explicit opt-in controls
- enable copy, download, fullscreen, and pan/zoom in Desktop
- preserve initial bundle size through dynamic Mermaid imports
- cover lazy loading, strict security, failures, and streaming fences
Refs #13568
* test(desktop): harden Mermaid rendering coverage
* test(desktop): stub scrollTo for streamdown 2.6 in Markdown interaction tests
streamdown 2.6 (pulled in by main) scrolls streaming code blocks with element.scrollTo, which jsdom does not implement. Stub it the same way chat-messages.test.tsx and channels-view.test.tsx already do.
* fix(ui): stop Mermaid diagram links from bypassing host link policy
Mermaid's `securityLevel: "strict"` blocks script execution and dangerous URL
schemes, but still renders a `click <node> "https://…"` directive as a live
`<a xlink:href>` anchor inside the SVG. Streamdown injects that SVG with
`dangerouslySetInnerHTML`, so those anchors never pass through the `a`
component a product supplies — bypassing the deceptive-link confirmation and
the external-open routing that ordinary Markdown links go through, with no
rel="noreferrer", and letting a click navigate the desktop webview away from
the app itself.
A diagram label is authored independently of its destination, so these links
are deceptive by construction: the desktop app's own isDeceptiveLink rule
would flag every one of them.
- neutralize rendered diagram links in @cline/ui so they cannot navigate on
their own, preserving an http(s) destination on data-cline-diagram-href
- reattach them in Desktop to the existing confirmation modal, which shows the
real destination before anything opens
- recover the anchor from pointerdown: Streamdown's pan/zoom surface takes
pointer capture, so a real click is retargeted away from the anchor; a press
that moves more than a few pixels is treated as a pan, not a link click
- drop non-http(s) destinations rather than surfacing them, so a renderer
change cannot widen what the app will open
- assert the packed Mermaid control shape instead of a "not false" check that
also accepted undefined or an empty object
Verified against real Mermaid 11.16.1 in a browser: script, event-handler,
iframe, foreignObject and javascript:/data:/vbscript:/file: payloads are all
neutralized by strict mode, and `%%{init}%%` / YAML frontmatter cannot
downgrade securityLevel. The http(s) `click` anchor was the one live escape.
Stacked on #13721, which adds the shared lazy Mermaid plugin this hardens.
Refs #13568
Co-authored-by: yiaany <ilyaantonievich@gmail.com>
Co-authored-by: yiheng-kkk <lij921093@gmail.com>
* feat(ui): theme Mermaid diagrams with the Cline design system
Replace Streamdown's built-in Mermaid block in Desktop with an owned
`@cline/ui` block (`createMermaidRenderer` from
`@cline/ui/components/mermaid-block`):
- Mermaid's `base` theme with variables resolved from the live design tokens
(`--card`, `--foreground`, `--primary`, ...), following light/dark and the
accent palette and re-rendering when either changes; a diagram's own
`%%{init}%%`, frontmatter `config:`, `classDef` and `style` still win
- a filename header from the fence title (```mermaid title="my-diagram"),
frontmatter `title:`, or a name derived from the diagram
- zoom, fullscreen, copy, and PNG / `.mmd` downloads; PNGs are opaque, 2-3x,
capped at 4096px and kept under the image attachment limit
- renders are serialized through one queue, since Mermaid is a process-wide
singleton that is not safe to render concurrently
The owned block injects the SVG itself, so diagram link neutralization moves
into the shared render service and covers both this block and the Streamdown
diagram plugin. Desktop keeps routing diagram link clicks through the existing
confirmation modal.
Refs #13568
* fix(ui): avoid polynomial backtracking in Mermaid helpers
* fix(ui): make diagram label extraction linear-time
Replaces the backtracking label-regexes CodeQL flagged (ReDoS, PR #14757) with single-pass scanners and line-column parsing.
* refactor(ui): share the Mermaid render service and drop dead naming helper
createLazyMermaidPlugin now delegates import/retry/queueing/link-neutralization to createMermaidService instead of duplicating that path, keeping only theme-keyed config. Removes deriveMermaidDiagramName, which had no consumer outside its own tests.
---------
Co-authored-by: harrisonofcline <harrison@cline.bot>
Co-authored-by: yiheng-kkk <lij921093@gmail.com>
* fix(desktop): reconnect chat sessions after Cline Hub loss
Retry session rebuild for up to a minute when the hub transport closes or the session is missing, then end the turn as interrupted so the user can resend. Surface reconnect status in the webview instead of treating it as an error.
* fix(desktop): scope hub-loss recovery to lost sessions
- Probe the hub before rebuilding so a session it still holds is left alone
- Cancel the reconnect loop on stop/abort/reset and undo a racing rebuild
- Recover queued sends too, re-queueing once the session is back
- Show the resend notice as an error-role bubble that history sync keeps
- Recognize missing sessions wrapped as generic hub command failures
* fix(desktop): share hub recovery and preserve session ownership
* fix(desktop): isolate replacement recovery and avoid duplicate queued sends
* fix(desktop): serialize recovery across session replacements
* fix(desktop): bound the entire shared recovery lifecycle
* desktop: add Connectors 'What's new' catch-up
* desktop: tighten Suggested combos copy to fit two lines
* desktop: give the Connectors catch-up its own spotlight layout with an Open Connectors CTA
* desktop: move connector logos and pitch into the What's new body, make the example clickable
* desktop: let the What's new dialog scroll in short windows
* desktop: browse connectors from Customize and make Connectors the default tab
Customize > Connectors gains a Browse section below the suggested recipes
that lists the rest of the Composio catalog with search and infinite
scroll, so nobody has to open the Marketplace just to find a connector.
Connectors is now the first and default Customize tab (falling back to
Tools for accounts without beta access).
Connector rows and cards always show the connector's own brand logo:
ConnectorLogo falls back to Composio's slug-based logo URL, and the
Marketplace connector rows use it instead of the generic type glyph.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: format connectors view
* desktop: point the empty Connectors state at Browse and in-chat connect
Connectors can now be connected from inside a task (cline/cline#14755),
so the empty state no longer sends people to the Marketplace.
* desktop: render connector logos at full tile size
ConnectorLogo was drawn at size-5 inside size-7/8/10 tiles, leaving a
grey margin around every mark. The image now fills the tile itself
(size and radius come from className) and the wrapper spans are gone;
the lucide/letter fallbacks keep the tile background.
* desktop: always show the Connectors tab with sign-in and beta guidance
The Connectors tab no longer depends on Composio availability, so the
default tab never swaps underneath the user. When connectors are
unavailable the tab explains why: signed out shows a Cline sign-in card
(same device-code OAuth flow as the Account page) plus Create account;
signed in without beta access shows a rollout notice with Check again.
The tab count is hidden while connectors are unavailable.
Also addresses review feedback: the search hint counts only the Browse
entries actually hidden, and a failed logo retries when a different URL
arrives.
* desktop: separate Installed and Browse searches, wait for account before sign-in prompt
Each Connectors list now has its own search field: the top field filters
Installed (and is hidden when nothing is installed), the field under the
Browse header filters the catalog. Suggested is no longer filtered.
AccountProvider exposes accountReady so the unavailable card can show a
spinner until the first identity lookup finishes instead of telling a
signed-in user without beta access to sign in.
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* feat(desktop): add /compact slash command
Suggest /compact in the composer's slash menu and run it in the sidecar
like the CLI's local command: summarize the transcript with a manual core
compaction and persist it as the session's compaction state instead of
sending the literal text to the model.
* fix(desktop): forward session baseUrl and headers to /compact
Build the compaction connection with buildSessionConnectionUpdate so a
session's top-level baseUrl and headers reach the summarizer like they
reach the live session, and cover the successful compaction path.
* desktop: add marketplace design exploration with five layout variations
Exploration-only /design/marketplace route (not linked from the app) that
renders the same unified item list through five candidate directions:
typed rows, storefront, tabs like Customize, one row per service, and a
directory table. Catalog entries are live; connectors are a fixture.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: make marketplace rows self-describing by type
Every row and the detail header carry the type's glyph on a per-type
tint, section headers explain what each type is, and rows in mixed
search results show a type pill, so same-named entries (Figma the MCP
server vs Figma the connector) read as different things. Category tags
move from a second row of pills into a single dropdown. Removes the
design-exploration route now that direction A is chosen.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: drop the count from marketplace section headers
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
activeHistorySessionId was derived only from thread.historySession, which
is set when a session is opened from history. A session started fresh in
the app only gets thread.sessionId bound via thread-started, so the
sidebar never received an active id for it and its row stayed
unhighlighted (and its own chat events marked it unread).
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: align Customize > Connectors tab with the other Customize tabs
The Connectors tab rendered a lone 'Browse all connectors in the
Marketplace' button when empty (redundant with the page header's
Marketplace button) and a compact two-column grid when populated,
neither matching Skills / Plugins / MCP.
It now mirrors those tabs: description row with a refresh button,
full-width search, an Installed section with a count and stacked cards,
and the dashed empty-state box. The recommended connectors that are not
yet connected appear in a Suggested section with one-click Install, so
the empty state is never a dead end without linking out.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: drop Suggested section from Connectors tab, keep Installed only
Matches the Plugins / MCP tabs exactly: the empty state is the dashed
box pointing at the marketplace, and the header Marketplace button is
the single way to browse.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: address review on Connectors tab refresh and card click target
- A failed manual refresh keeps the installed list and refresh button and
shows an inline error instead of replacing the whole tab.
- A refresh response that arrives after a newer connect/disconnect write
is discarded (status version guard), so stale snapshots cannot
resurrect an uninstalled connector.
- Successful refreshes go through applyStatus so the Customize tab count
updates too.
- The whole connector card is the click target for the detail dialog,
matching its hover treatment and MarketplaceEntryCard.
- Tests cover the refresh button and inline refresh error.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: suggest connector recipes on Customize > Connectors
Adds a Suggested section under Installed with four connector
combinations (incident RCA, spec-to-shipped, launch-and-market,
support-to-PRs), each explaining the use case, an example prompt, and
per-connector install chips. Connected members show a check; a recipe
disappears once all of its connectors are connected. Shown in both the
empty and populated states; search filters recipes too.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: add 'Organize your day' connector recipe (Gmail + Slack + Google Calendar)
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: drop the GitHub connector recipe (GitHub is not an available connector)
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(desktop): show installed tool count, not catalog total
Drop the connector slug and the stale catalog tools total from the detail dialog so the count reflects only tools available in new sessions.
* fix(desktop): show zero installed tools instead of catalog total for empty connectors
* fix(desktop): match web cloud model catalogs
* refactor(desktop): reuse existing cloud model picker
* fix(desktop): require a ready cloud model and allow catalog retry
* fix(desktop): load local cloud model capabilities explicitly
* fix(desktop): start cloud model selection without local fallbacks
* fix(desktop): remember the Cloud/Local pick and cloud model for new threads
Every new thread seeded its config from `getInitialChatConfig()`, which
restored the last provider/model/workspace but never the execution
target, so a user who ran a task on Cloud was silently dropped back to
Local on the next "New chat".
Persist the user's Cloud/Local pick (and the model last chosen while on
Cloud, which the model picker deliberately keeps out of the per-provider
local memory) in localStorage, and seed new threads from it. Only the
user's own selection is written; the flag-off fallback that forces Local
never overwrites it. The cloud-agents flag now starts as "unknown" so a
thread remembered on Cloud is not bounced to Local before the flag has
resolved.
Part of #14492 (desktop half). The web half is cline/core-platform#3395.
* fix(desktop): open Cloud on the remembered cloud model when switching mid-thread
`getInitialChatConfig()` already seeds a new Cloud thread from the
remembered cloud model, but the Local → Cloud switch inside an open
thread still derived the model from the current local one, so the picker
showed a different model than the one the user last chose on Cloud (and
the next new chat would show the remembered one again). Read the
remembered cloud model there too.
Found by e2e; also flagged by Greptile/Codex on #14619.
* fix(desktop): honour an explicit Local pick and keep validated workspace on Cloud → Local
Three review follow-ups on the remembered execution target:
- Picking "Local" in the environment menu from an SSH host now also
selects the local execution target, so the new local draft does not
open on Cloud just because Cloud was remembered. Choosing another SSH
host is not a Cloud/Local decision and leaves the memory alone.
- A thread that opened on Cloud never captured local settings; switching
it to Local now keeps the workspace that discovery already validated
instead of re-applying the raw remembered path, which may no longer
exist.
- While the cloud-agents flag is still unknown, a composer remembered on
Cloud keeps its cloud controls rather than showing the local folder
picker; if the flag settles false the existing effect moves it to Local.
* fix(desktop): never leave a Cloud → Local switch with an empty workspace
If a thread that opened on Cloud is switched to Local before workspace
discovery has returned, nothing was captured and the Cloud config's
workspace is still empty, so the Local composer ended up with no
workspace and the persistence effect could record "" as the last
workspace. Fall back to the remembered path in that window; discovery
still validates and overrides it when it completes.
* feat(desktop): remember the cloud repository and branch for new threads
Remembering the Cloud target and model but making the user re-pick the
repository on every new thread was the remaining gap: the desktop never
had repo memory (only reopened sessions carried theirs). Store the last
cloud repo and branch alongside the target/model, seed new Cloud threads
and Local → Cloud switches from them, and write them from the repo and
branch pickers. Changing the repo forgets the branch until one is picked
for it; the composer's existing accessibility check still clears a repo
the current account can no longer reach.
* fix(desktop): keep the remembered cloud repo through composer clears and history
- The composer clears the repo/branch itself when the current account
cannot reach the repository (GitHub not connected, no access), and
that clear went through the same handlers that persist a pick, erasing
the memory. Only non-empty picks are remembered now.
- reset() only re-seeded the repo when the execution target changed, so
a new chat after viewing another Cloud session kept that session's
repository. Re-seed the repo and branch from the remembered selection
regardless of target, matching provider/model.
* fix(desktop): keep the remembered cloud repo through the first setup check
The composer's mount-time cloud setup check invalidated the scope
eagerly, clearing the repo and branch before confirming access. Since
every new thread mounts a fresh composer, the remembered repo was seeded
and then wiped on each launch and each Local → Cloud switch, and nothing
put it back once access was confirmed.
Only a switch to another user invalidates eagerly now; the first check
of a composer keeps the remembered repo and the existing access guard
drops it if the result shows it is no longer reachable. The sidecar's
scope-change broadcast still invalidates as before.
* fix(desktop): judge a re-activated Cloud composer only by fresh access results
When the composer comes back to Cloud (e.g. after reconnecting GitHub
while on Local), the access guard ran in the same render against the
previous activation's cloud setup results and cleared the remembered
repo before the new check had returned; the successful refresh then had
nothing to restore. Track which request the setup state reflects and let
the guard act only once it matches the latest one. Re-activation also
marks the scope as checking again (without clearing the repo), as it did
before a user-switch became the only eager clear.
* fix(desktop): wait for repository access before starting cloud tasks
* fix(desktop): recover from superseding repository check failures
* fix(desktop): retain validated composer on picker refresh errors
* fix(desktop): share concurrent cloud repository checks
* fix(desktop): persist target only after environment switch succeeds
* fix(desktop): coordinate local selection with disconnect events
* refactor(desktop): reuse model preferences and narrow persistence scope
* fix(desktop): remember Cloud repository and branch for new chats
* fix(desktop): expose recovery for Cloud branch restore failures
* fix(desktop): remember explicit Local selection after fallback
* chore(desktop): trim persistence comments
* fix(desktop): resume Cloud restore after picker failure
* llms/core: register catalog custom providers with the gateway on the agent run path
Custom providers added via Desktop's Add Provider dialog (persisted to
providers.json/models.json and registered in the @cline/llms model
catalog) were visible in every picker but failed at execution time with
'Unknown or disabled provider "<id>"'. The agent run path
(createAgentModelFromConfig -> createGateway) only seeded builtin
provider registrations, while the legacy ApiHandler path already bridged
catalog collections into the gateway via resolveProviderRegistrationSync.
Export that bridge from @cline/llms as
resolveGatewayProviderRegistration(Sync) and apply it in
createAgentModelFromConfig, mirroring the ApiHandler path.
Fixescline/cline#14180
* desktop: resolve MCP settings through one canonical resolver
The sidecar carried its own homedir-based resolveMcpSettingsPath next to
the shared @cline/shared/storage resolver, and the Tauri shell had a
third copy. All three agree under default env, but the duplicates
ignored CLINE_DIR/CLINE_DATA_DIR (and the shared resolver's HOME
handling), so MCP reads/writes, the OAuth authorize command, and the
'Open MCP settings' window action could target different files.
Delete the sidecar duplicate so every sidecar command uses the shared
resolver, and mirror the shared resolver's precedence
(CLINE_MCP_SETTINGS_PATH > CLINE_DATA_DIR > CLINE_DIR/data >
~/.cline/data) in the Rust open_mcp_settings_file path.
Fixescline/cline#14152
* refactor(desktop): resolve provider metadata in the host
* Authenticate provider model source requests
* Avoid iterable Headers requirement in VS Code compatibility build
* Refresh source catalogs when provider credentials or endpoints change
* fix(core): save provider settings when model discovery fails
* fix(core): propagate provider catalog persistence failures
* fix(core): restore provider settings when catalog writes fail
* fix(core): serialize provider mutations and persist settings once
* fix: preserve provider customizations and handle unknown auth metadata
* fix(core): track discovery ownership on initial catalog refresh
* fix(desktop): address provider catalog review feedback
- Treat an empty source catalog as best-effort during credential saves
- Prune models from source-backed entries saved before discovery ownership
- Format turn failures with the submitted provider's auth metadata
- Re-export ProviderLocalCli/resolveProviderLocalCli from @cline/llms for compatibility
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>