Bun cannot hand an async virtual module to require(), so a CommonJS plugin
(or a CommonJS dependency) calling require("@cline/sdk") failed with
"require() async module is unsupported". Synchronous loaders serve both
import and require; the bundler still emits each one as a lazy chunk load.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Plugin installs drop their @cline/* dependencies because the host provides
them, but a compiled CLI had nothing on disk to provide them from, so a
CLI-managed Hub (desktop or an SSH remote, which gets only the binary) failed
plugins with "Cannot find module '@cline/sdk'".
Compile the plugin worker bootstrap into the CLI as an extra entrypoint and
point CLINE_PLUGIN_BOOTSTRAP_PATH at it. The worker registers @cline/sdk,
@cline/core, @cline/shared and their public subpaths through Bun's runtime
plugin API, loading each lazily from the executable's own build, and plugins
then load with Bun's native import() since jiti would not see those modules.
Plugin dependencies of their own still resolve from their node_modules.
* 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.
* fix(lmstudio): disable undici's idle-body timeout so slow local generations aren't killed mid-stream
* fix(lmstudio): compose the bodyTimeout override onto the active dispatcher instead of replacing it
* fix(vscode): disable undici idle timeouts for local model providers
Node's fetch applies undici's default 300s headersTimeout/bodyTimeout,
each measuring time since the last byte received. Local servers
(LM Studio, Ollama) legitimately go silent for longer while processing a
large prompt (headers arrive immediately, then no body bytes until the
first token), so long requests died mid-task with
'terminated: BodyTimeoutError (UND_ERR_BODY_TIMEOUT)' even though the
server was still generating.
Route LM Studio and Ollama inference through a fetch bound to an
EnvHttpProxyAgent with headersTimeout/bodyTimeout disabled. This calls
undici's own fetch rather than global fetch because VS Code's
http.fetchAdditionalSupport patch replaces caller-supplied dispatchers
with its own default-timeout Agent. Cancellation stays with each
request's AbortSignal.
Fixes#13464
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(vscode): disable undici idle timeouts for providers that may run a local model
Combine the two earlier drafts: dispatch through undici's own fetch so the
override survives VS Code's global fetch wrapper, and keep the env-var
proxy agent so JetBrains proxy support is unchanged. Widen the provider
set from LM Studio and Ollama to also cover OpenAI Compatible and LiteLLM,
which telemetry shows carry most of the UND_ERR_BODY_TIMEOUT failures.
The net test now exercises real undici fetch against a local server that
flushes headers and then stays silent, with a short-timeout control to
prove the silence is long enough to trip the default timer.
* test(vscode): keep the idle-timeout net test off any machine-level proxy
* chore: drop the changeset file, the repo no longer uses changesets
* fix(vscode): only skip idle timeouts when the base URL is on the local network
OpenAI Compatible and LiteLLM often point at remote corporate gateways.
On VS Code, fetchWithoutIdleTimeouts bypasses the host's proxy handling
(http.proxy, OS/PAC proxy, proxyStrictSSL), so routing those through it
would break proxied setups. Gate on the base URL as well: loopback,
private/link-local/CGNAT ranges, single-label hosts and .local/.internal
names get the idle-timeout-free fetch; remote gateways keep the
proxy-aware fetch and its dead-connection cap.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(vscode): keep loopback direct in fetchWithoutIdleTimeouts, cover 0.0.0.0 and mapped IPv4
EnvHttpProxyAgent sends loopback through http_proxy whenever no_proxy is
unset, which VS Code's own resolver never does. Pick a plain Agent for
loopback hosts and the env-var proxy agent for everything else, both
with idle timeouts off. The local-network gate now also accepts 0.0.0.0
and IPv4-mapped IPv6 addresses, which Ollama and LM Studio print when
bound to all interfaces.
The net test now runs with HTTP_PROXY set and NO_PROXY unset against a
recording CONNECT proxy: loopback never touches it, an unroutable host
goes through it, and neither path has an idle timeout.
* fix(vscode): match loopback by address so IPv4-mapped IPv6 stays direct
The URL parser rewrites [::ffff:127.0.0.1] to [::ffff:7f00:1], which a
string match on "127." misses. Use a BlockList, which checks mapped
addresses against the IPv4 rules, for the loopback bypass and drop the
now-redundant unwrapping in the local-network gate.
---------
Co-authored-by: lizhe <marshal_li_b@163.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* test(llms): follow models.dev catalog changes for v0 and OpenCode Go
models.dev dropped the v0 provider and moved OpenCode Go from /zen/go/v1
to /inference/go/v1 (both endpoints serve the same model list).
* chore(llms): refresh generated model catalog
* fix(cli): let the compiled binary resolve plugin SDK imports natively
Plugins import @cline/core from the wrapper's node_modules. Bun standalone
executables skip package.json loading at runtime by default, so every bare
import from those files failed to resolve natively and jiti fell back to
babel-transpiling the whole SDK graph (~440 files, ~8s cold). That blew the
8s plugin load timeout after an upgrade or reboot and left the plugin
failed for the life of the hub. With autoloadPackageJson the cold load
takes ~1.4s, matching the 3.0.70 subprocess sandbox.
* fix(cli): show the resumed transcript when startup posts a notice
Resuming a session hydrates the transcript after the session starts. A
plugin load failure posts a status notice during that start, and the
hydration guard treated any existing entry as user activity and skipped
the transcript, so the resumed session looked empty. Skip only once the
user has submitted, and keep startup notices after the transcript.
* Revert "fix(cli): show the resumed transcript when startup posts a notice"
This reverts commit 0fd43afc70.
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.
* 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>
Conflict resolutions:
- scripts/build-sidecar-bin.ts stays deleted. main's change to it (#14928:
stop UPX-packing the Linux SSH binaries, which breaks Bun 1.4's reads of
its own embedded payload) is carried over to build-desktop-runtime.ts,
which no longer UPX-packs the Linux Cline CLI copies it bundles for SSH
remotes.
- desktop-publish.yml drops the upx installs, as on main, and main's new
SSH smoke test now runs the bundled remote CLI through the same
`hub ensure` / `hub stop --discovery-path` the desktop sends over SSH.
- hub.ts keeps main's per-subcommand hub options (#14900) and adds this
branch's --discovery-path to them, so it works before or after the
subcommand, alongside the ensure/start flags (--json,
--allow-port-fallback, --no-connectors, --login-shell-path).
- The desktop README keeps this branch's CLI-based SSH description with
main's note that the binary must not be UPX-compressed.
* feat(vscode): show dedicated ai& provider settings
Selecting ai& in the extension showed the generic OpenAI-compatible form
(base URL, model ID, custom headers, Azure options). Register aiand as a
known provider so it gets the standard API key + model picker form, backed
by the SDK builtin (fixed base URL https://api.aiand.com/v1, catalog models,
default zai-org/glm-5.3).
* chore: drop unused changeset file
* fix(shared): stop billing image base64 payloads at CHARS_PER_TOKEN (#14512)
estimateRequestInputTokens and createTokenEstimator both derive their
token estimate from JSON.stringify(x).length, which charges an
ImageContent block's base64 data string at the 3-chars-per-token rate
used for text. A single ~90KB base64 image (a very ordinary screenshot)
was reported as ~30k tokens when real vision-model costs top out around
1.6k, so a handful of images could trip shouldCompact well before the
transcript is actually near budget.
Add createImageAwareReplacer(), a JSON.stringify replacer that blanks
the data field of any {type: "image", data: string} node wherever it's
nested, plus a counter, and use it at both call sites. Add a flat
ESTIMATED_TOKENS_PER_IMAGE (1600) back onto the char-based estimate per
image found, so images still count toward the budget without scaling
with their base64 length.
The catch-fallback path (serializeMessage) already renders images as
"[User image]: mediaType" with no base64, so this was specific to the
primary JSON.stringify path.
* fix(shared): cover AgentImagePart shape and stop fallback double count
createImageAwareReplacer only matched ImageContent's {type:'image',
data:string}. AgentMessage's image part (agent.ts) carries its payload
under 'image' instead, and agent-message-codec.ts populates it with the
same base64 string; that shape reaches estimateRequestInputTokens via
gateway.ts on every streamed request and was still billed at
CHARS_PER_TOKEN. Blank the 'image' field too when 'data' isn't present.
Separately, both call sites read imageCount() even when the primary
JSON.stringify(..., replacer) pass threw partway through and fell back
to a full re-serialization (which includes images' raw base64). Images
counted before the throw then had ESTIMATED_TOKENS_PER_IMAGE added on
top of their already-included char cost. Only capture the count when
the replacer-driven pass completes.
* test: pin the flat per-image token charge
* fix: load symlinked rule files from config directories
* fix(core): automatically reload symlinked instruction targets
* fix(core): drop symlink target watching, keep link-path loading
Review feedback on #14912: reading rules through the link path already
fixes the reported bug, and each new session re-reads the rules. Watching
wherever links point is a larger change than the bug needs and carried
loose ends (directory links in the target path, a readlink per file on
every refresh).
- revert the target-directory watching, chain walk and broken-link
retention from ab59fbca4
- keep stat-based symlink acceptance in the scanners, so a broken link
is skipped as on main
- DOC.md: edits to a symlink's target are picked up on the next refresh
or new session
* 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>
* Default to the first Cline recommended model (Sonnet 5.5 fallback) across CLI, extension, and desktop
- @cline/shared: CLINE_DEFAULT_MODEL_ID is now the anthropic/claude-sonnet-5.5 fallback
- @cline/llms: the cline provider's defaultModelId prefers GENERATED_CLINE_RECOMMENDED_MODELS.recommended[0]
- CLI: resolve the provider's SDK default before falling back to the first catalog entry; drop the hardcoded sonnet-4.6 fallback
- VS Code: openRouterDefaultModelId -> anthropic/claude-sonnet-5.5; cline provider falls through to the SDK default
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Default the SDK openrouter provider to anthropic/claude-sonnet-5.5
The VS Code extension's OpenRouter model field is populated from this SDK
default via resolveModelInfo, so the extension-side openRouterDefaultModelId
constant alone did not change the fresh-install selection.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Desktop: adopt the provider catalog default model once the catalog loads
A fresh install booted on CLINE_DEFAULT_MODEL_ID and kept it when the user
skipped onboarding, so the chat bar never picked up the Cline provider's
recommended-list default. Swap the bootstrap fallback for the catalog
default unless a model was already remembered for that provider.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Scope the recommended-model default to the Cline providers
Only cline/cline-pass declare a curated default; other manifests may expose
a placeholder like "default" (ollama, lmstudio), so the CLI keeps falling
back to the first catalog entry for them. The desktop bootstrap swap now also
applies only to new Cline chats, never to hydrated sessions.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Test both branches of the cline default model selection
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Simplify: drop the desktop bootstrap swap and connector catalog lookup
The desktop app keeps CLINE_DEFAULT_MODEL_ID as its pre-onboarding default
(onboarding already stores the provider's recommended default), and the
connector only swaps its hardcoded literal for the shared constant.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* CLI: only the cline provider uses the SDK default before the catalog
cline-pass's manifest default is just the first bundled model, not a curated
pick, so it keeps the live catalog's own ordering.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Connectors: prefer the provider's declared default over the Cline fallback
Non-Cline connector sessions with no saved model were handed a Claude id.
Use the provider manifest default when it is a real model, skipping the
"default" placeholder that catalog-less local providers expose.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* feat(core): in-chat connector authentication via Composio meta tools
Signed-in beta sessions create a meta-tool session through the Cline API
connectors proxy and register COMPOSIO_MANAGE_CONNECTIONS and
COMPOSIO_WAIT_FOR_CONNECTIONS, so the agent can hand the user a Connect
Link when a task needs an app that is not connected yet.
* test(core): verify connector meta-tool usage telemetry
* fix(core): tell the agent when to use connector meta tools
Composio's descriptions are generic, so models answered "I don't have
access to Gmail" instead of offering a Connect Link.
* fix(core): replace Composio meta-tool descriptions with our own
Composio's descriptions require COMPOSIO_SEARCH_TOOLS first, which this
session does not expose, so models refused to start the connection flow.
Also drop the provider's session_id input; the proxy supplies it.
* feat(core): expose Composio search and multi-execute meta tools
Register every meta tool the server allows with Composio's own
descriptions and schemas, which reference each other (search, manage,
wait, multi-execute). Drops the custom descriptions.
* fix(core): require the complete Composio meta-tool flow
* 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>
The `hook` command's dispatcher had no case for `agent_error`, which the
shared hook schema accepts and the CLI runtime emits when a run fails. A
valid `agent_error` payload fell through to the default branch, so the
command exited 1 without a JSON response. It now returns `{}` like the other
agent events. Adds the first unit tests for the command.
Fixes#14808
* 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
The TUI's startup check now accepts a provider API key from the environment
(e.g. OPENROUTER_API_KEY) when the provider declares exactly one environment
variable, as the runtime does when no key is saved. The key is checked through
isProviderSettingsUsable, so the provider's other requirements still apply.
Providers with several environment variables (Bedrock, Vertex, ...) are
unchanged, and nothing is persisted.
Related to #14923
* docs: match the CLI automation pages to the CLI
The scheduling, teams, subagents, connector and sample pages told
readers to run commands and flags the CLI doesn't have, and the
samples downloaded files that are gone.
- Scheduling: `executions` is `history`; `--workspace` is required;
provider and model are separate flags; the other create options,
subcommands and cron names are listed. Routing results now names the
chat with `--delivery-thread` or `/schedule create` in the chat. A
schedule with only `--delivery-bot` never posts anything.
- Teams: start with `/team` and resume with `cline --id`, not the
hidden `--team-name`; there is no `--no-teams`; state is in
teams.db. The SDK teams guide gets the same CLI fix.
- Subagents: the page described the old read-only research tool.
`spawn_agent` subagents can edit files and run commands without
further approval, run in the CLI and Desktop only, and are turned
off with the `spawn_agent` tool.
- Connectors: gchat, WhatsApp and Linear flag names; Discord tools
are on by default.
- Samples: inline the scripts whose download links 404; filter
`run_result` for the final answer; `cline auth` needs `--modelid`;
current model ids and prices; the issue responder runs Cline with a
read-only token and posts from a second job, like the PR reviewer.
Part of #14853.
Co-authored-by: Harshil Mistry <125483347+harshil-mistry@users.noreply.github.com>
* docs: check commenter permission, fail scripts on failed runs
The @cline issue responder admitted any OWNER, MEMBER or COLLABORATOR,
and organization members or collaborators with read access pass that
check. A secret-free authorize job now looks up the comment author's
repository permission and lets the analyze job run only for write or
admin. It checks the comment's author rather than the actor, so editing
someone else's comment doesn't start Cline on their text. The analyze
job also stops setting CLINE_DIR from runner.temp, which isn't
available at job level.
Plan mode removes the editing tools and refuses common file-changing
commands, but python -c and node -e still write files. The scheduling
and subagent pages now say so instead of promising that Plan runs
can't edit files.
When a run fails, the CLI exits 1 but run_result still carries the
error text, so jq prints it and the pipeline succeeds. The orchestration
scripts now use set -euo pipefail, wait on each background review by
PID, remove output files from earlier runs and name their inputs
instead of globbing. analyze-issue.sh sets pipefail. The PR review
pattern names the pull requests the sample skips.
* docs: wait on saved review PIDs, list the current team tools
The parallel review samples waited on $(jobs -p). Bash 3.2, the macOS
/bin/bash, leaves finished jobs out of that list, so a review that failed
fast was never waited on and the next model read an empty review. Save
$! after each background pipeline and wait on those PIDs.
The SDK teams guide listed team_delegate_task, team_check_status and
team_get_result, which no longer exist, and a JSON layout under
~/.cline/data/teams/. List the tools in TEAM_TOOL_NAMES and say that team
state is in ~/.cline/data/db/teams.db, keyed by session and restored
when the session resumes.
---------
Co-authored-by: Dominic Cooney <55120+dominiccooney@users.noreply.github.com>
Co-authored-by: Harshil Mistry <125483347+harshil-mistry@users.noreply.github.com>
* fix(vscode): keep error details in production logs
Logger dropped every extra argument unless IS_DEV was true, and production
bundles build IS_DEV as false, so lines like 'Cline OAuth login failed:' lost
the error. Production now appends error messages (with cause chain), numbers
and booleans; strings and objects stay dropped because call sites pass OAuth
URLs, terminal and file contents and settings. Verbose mode now serializes
Errors as stacks instead of {} and survives circular arguments. The
'Opening browser' line logs only origin and path.
* fix(vscode): reduce URLs in production log output to their origin
Error messages printed in production can embed URLs with credentials in
the query, path or userinfo, such as the expanded MCP server URL in the
invalid-URL error or Bun's URL parse error echoing its input. Reduce
every URL in a production error message to its origin, log only the
origin when opening a browser, and echo the MCP server URL as entered
rather than with environment variables expanded.
* fix(vscode): redact whole URLs in log text and keep the matcher linear
Parentheses and brackets are valid URL characters, so ending a match at
them left the rest of a credential-bearing URL in the log. Match up to
whitespace, quotes or angle brackets instead, trim only trailing
sentence punctuation and unbalanced closing brackets, and bound the
scheme length so scanning stays linear on adversarial input.
* fix(vscode): move the URL redaction helper into shared
Logger lives in src/shared, which the webview build compiles too, and
there `@/` resolves to webview-ui/src, so importing `@/utils/redact-url`
broke the webview typecheck.
* refactor(vscode): drop free-text URL scrubbing from the Logger
Scrubbing URLs out of arbitrary error messages is a blocklist that can
never be complete. Give error messages the same trust as the log message
itself instead, and keep the deterministic fixes: the MCP invalid-URL
error no longer embeds expanded environment variables, and the browser
URL is logged as its origin at the one call site that logs it.
* fix(vscode): omit JSON parse error messages from production logs
V8 and Bun quote the parsed input in JSON.parse errors, and the parsed
input is often a settings file or an API response carrying credentials,
such as the remote-config provider API keys. Log only the error name.
* fix(vscode): keep the entered MCP URL out of the invalid-URL error
A URL typed with userinfo or query credentials failed validation and
was echoed in the error, which is logged in production. Omit the URL
and keep the guidance; the user just typed it and can see it in the
form. Also drop the changeset: the repo no longer uses them.
* feat(vscode): fingerprint redacted URLs in the browser-open log line
Append three hex digits of a SHA-256 over the full URL to the redacted
origin, so a log shows whether two lines with the same origin were the
same URL without revealing it.