* fix(hub): stop shipping full transcripts inside broadcast hub events
Every session.updated (and session.created/detached/run.started) event
embedded the session's ENTIRE message transcript via readCoreSessionSnapshot,
even though no consumer reads snapshot.messages off an event — clients fetch
messages with the session.messages command. For a multi-megabyte transcript
this turns every status flip into megabytes per subscriber, floods the
durable event log, and (until the send-queue backpressure fix lands) lets a
slow subscriber balloon the hub process by one full transcript copy per
event — reported as a 25GB cline process on a 16GB Mac.
Strip snapshot.messages centrally in HubServerTransport.publish() so every
current and future event publisher is covered, the event log stores slim
envelopes, and cursor replay stays byte-identical with live fan-out. All
other snapshot fields (status, usage, model, workspace, checkpoint) are kept,
and command replies are untouched.
* fix(hub): never capture the transcript into event/reply snapshots
Replaces the publish-boundary strip with the real fix: don't build
message-bearing snapshots in the first place. emitSessionSnapshot no longer
re-reads the entire transcript from disk on every status flip, and
readCoreSessionSnapshot no longer reads it for any event or reply — a
snapshot is a state notification (status, usage, model, workspace,
checkpoint); the transcript is fetched via the session.messages command.
Checkpoint-restore snapshots (session-versioning-service) are untouched:
restore replies carry messages in their own dedicated field.
* fix: install marketplace plugins and MCP servers in-process instead of spawning a cline binary
The desktop app sidecar and cline-hub shelled out to 'cline plugin install'
and 'cline mcp install' for marketplace installs. Packaged GUI apps inherit
launchd's minimal PATH on macOS and most desktop users have no cline CLI
installed at all, so installs failed with a red
'Executable not found in $PATH: "cline"' error.
Install via @cline/core's installPlugin/installMcpServer in-process instead,
matching what the VS Code extension already does. Also fix
parseMcpInstallArgs in @cline/core to treat the marketplace catalog's '--'
separator as end-of-options; previously the separator itself became the
stdio command.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* refactor: drop test-injection plumbing from marketplace installers
Call @cline/core's installPlugin directly instead of threading an
installer option through the marketplace entry points; tests stub the
core module instead.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* revert: keep cline-hub marketplace installs CLI-backed
The hub dashboard is launched via 'cline dashboard', so a CLI is always
present and CLINE_WRAPPER_PATH resolves it; the PATH bug only affects
the desktop app, which does not ship a CLI.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
Each session row shows a trash icon on the right while hovered (or
when the button itself is focused), opening the same delete
confirmation dialog the row's context menu uses. The row is a button
and buttons cannot nest, so the trash is an absolutely positioned
sibling inside a group/row wrapper, overlaid where the timestamp sits:
row hover hides the timestamp, shows the trash, and moves the row's
hover background to the wrapper group so it holds while the pointer is
on the trash itself.
Quitting the mac app beach-balled for ~5-7s. The shutdown POST was
built from the ws transport URL (appending /shutdown lands inside the
query string), so the sidecar was never told to exit, and stop() then
polled the child for up to 7s on the main thread - on macOS inside
applicationWillTerminate - before SIGKILLing it.
stop() now sends SIGTERM and returns immediately. The sidecar handles
SIGTERM with the same bounded (5s) graceful shutdown as the /shutdown
endpoint and exits itself, finishing session persistence as an orphan.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* feat(desktop): merge schedule details into one view and open run-now sessions
The schedule details dialog drops its Overview/Runs tabs: one scrollable
column with the meta grid, the configuration JSON (capped at max-h-64
with internal scroll so it cannot crowd out what follows), and a Runs
section beneath it showing the three most recent runs with a ghost
"Show all N runs" expander (collapsed again whenever a different
schedule's details open). The "Full configuration for this schedule"
subtext is gone; the dialog passes aria-describedby={undefined} so
Radix does not warn about the missing description.
Run now hands you into the session it starts. The trigger command
queues the run and returns before the runner attaches a session id, so
after the toast the handler polls the schedule overview once a second
for up to 15 seconds — which doubles as keeping the page's run status
fresh (refreshSchedules now returns the fetched overview to make that
single-stream) — until the triggered execution reports its session id,
then calls onOpenSession. Guarded so it never auto-navigates after the
user left the page.
* feat(desktop): hide runtime steering messages from transcripts
Scheduled/automation runs inject user-role steering messages each
iteration ("[SYSTEM] This run is not complete until you call
submit_and_exit...", plus a team-obligations variant). The chat view
rendered them as user bubbles, as if the person had typed them — in a
scheduled session the transcript was mostly [SYSTEM] noise.
They are machinery talking to the model, not something the person said
or needs to read, so the transcript now hides them entirely:
MessageBubble renders null for any [SYSTEM]-prefixed user message.
Grouping still treats them as working-row machinery via a single
isSystemSteeringMessage predicate — they collapse into the run's work
span, are never a turn boundary, can never be mistaken for a run's
answer, and never advance the run count even when metadata is missing —
so work-block folding and checkpoint/edit run numbering stay correct.
A finished scheduled session now reads as prompt, work summary, answer.
* fix(desktop): poll history while an attached session's event stream is dead
Opening a scheduled session while (or right after) it runs left the
view stuck on the thinking shimmer until the user switched away and
back. Root cause is in core: the hub daemon executes scheduled runs on
a private LocalRuntimeHost inside createLocalHubScheduleRuntimeHandlers,
while the hub server only projects live events from its own session
host — so session.attach succeeds but no assistant/tool/status events
ever flow. And since multiple hub daemons share cron.db, a run claimed
by a different daemon is invisible to this hub regardless. The proper
core rewiring is tracked as ENG-2474.
Client-side heal that covers every case: while an attached history
session reports a busy status and no chat_event chunk has arrived for
five seconds (and no assistant bubble is mid-stream), poll every three
seconds — re-read canonical history, merged through the same dedupe
path hydration uses, and the session record's status — so the
transcript and the thinking indicator settle in place. Locally driven
turns keep chunks flowing, so the quiet-window guard keeps the fallback
inert there.
* chore(desktop): format workspace selector components
Biome formatting drift that landed on main; picked up by a formatter
pass over components/views/chat.
* fix(desktop): keep stale-stream poll inert during locally driven turns
The fallback poll could fire between a local submit and the model's
first chunk (optimistic user bubble added, stream quiet past the
window, no assistant bubble yet). It then replaced the optimistic
bubble — raw prompt text — with its canonical history twin, which is
stored wrapped in a user_input envelope. The rekey handler that runs
when the stream starts looks for a trailing user bubble matching the
raw prompt, finds only the wrapped copy, and appends a second bubble:
duplicated messages in normal interactive chat.
The poll now stays inert while a local turn is in flight
(turnEpoch !== turnSettledEpoch, or outstanding optimistic user
messages), checked both before polling and again after the snapshot
returns. Hydration marks the turn settled — the mount defaults
(epoch 0, settled -1) otherwise read as an open turn and would keep
the fallback inert forever for the scheduled-session case it exists
for. Applying a polled snapshot also rebuilds the live tool routing
keys, same as hydration, so later tool events update canonical rows
in place instead of appending.
* fix(desktop): keep the working indicator alive for narrating scheduled runs
Watching a scheduled run live: the first tool row appeared, then the
thinking indicator vanished with nothing streaming, and the rest of
the run (final answer, submit_and_exit) only showed up seconds later
in one lump.
inferHydratedChatStatus treats a "running" session record whose
transcript ends on an assistant message as a session that died without
a status flip and reports "completed". That heuristic is right for
stale records, but scheduled/automation models narrate between tool
calls, so a polled snapshot can genuinely end on assistant text
mid-run — the completed flip hid the working indicator, folded the
run early, and disarmed the stale-stream poll (status left the busy
set), dead-ending live updates until an in-flight poll happened to
deliver the finished run.
The heuristic now only applies once the transcript has actually gone
quiet (newest message older than two minutes — comfortably past model
latency plus tool runs). A recently active transcript keeps the
record's "running" verdict, so the indicator stays up and polling
stays armed until the record itself settles.
* fix(desktop): stale-stream poll mirrors the session record instead of inferring
Replaces the previous fix for the vanishing working indicator (the
time-window guard added to inferHydratedChatStatus) with a version
that adds no inference at all: the heuristic is restored to exactly
its long-standing form, and the poll now maps the session record's
status verbatim (mapSessionRecordStatus).
The record is the right authority in the poll's context: the sessions
this fallback serves have a live host maintaining their record, and it
flips to a terminal status when the run ends. Transcript-shape
inference belongs only where it has always lived — hydrating sessions
whose records may be orphaned — and would misread a mid-run snapshot
ending on assistant narration as a finished session, hiding the
working indicator and disarming the poll.
* fix(desktop): address review findings on steering detection and run-now matching
Steering detection additionally requires the injected-message marker
(meta.userRunSpan === 0) beside the [SYSTEM] prefix, so a person's
genuine prompt that happens to start with "[SYSTEM]" stays visible
and turn-counted. The failure direction is deliberate: an unstamped
injected reminder would merely show as a user bubble, while the
content-only check could hide a real prompt.
Run-now only follows the execution id the trigger reply itself named;
the newest-execution-for-this-schedule fallback could open a previous
run's session when the trigger failed to enqueue one.
* fix(desktop): report a failed run-now instead of confirming a start
A trigger reply without an execution means no run was enqueued (the
schedule may have been disabled or deleted since the page loaded). The
handler previously toasted "Run started" regardless and then silently
skipped the session-open polling. It now shows a destructive
"Run not started" toast, refreshes the schedule list so the row
reflects reality, and skips the polling entirely.
* feat(desktop): split Customize into Installed and Marketplace pages
The Customize hub previously embedded a Browse section inside every tab
that had a catalog. That inlining made each tab long and buried the
catalog. Customize is now the installed inventory only (skills, MCP,
plugins, rules, hooks, tools tabs pass marketplaceVariant="installed"
to the embedded MarketplaceView; McpServersContent grew the same prop),
with an outline Marketplace button in the header.
Browsing moved to a dedicated Marketplace settings section that renders
the previously dead "directory" variant of MarketplaceView: one list
across all catalog types with type-filter chips, wrapping tag chips,
and light rules separating the filter tiers from each other and from
the results. The Clear control now renders inline at the end of the tag
row only while a tag is active, the Updated date is gone, and the
header hosts an Installed button mirroring the one on the Customize
page. Directory subheader copy: "A curated set of plugins, MCP
servers, and skills from the Cline community."
Tag and type chips wrap to new lines instead of scrolling
horizontally.
* feat(desktop): sidebar time view with sections, sort toggle, and scheduled detection
Restores the time-sorted session list as the default sidebar view, with
collapsible Pinned / Scheduled / Tasks sections (headers appear only
once something is pinned or scheduled) and the page-fill effect that
grows the fetched history window until a Show-more click makes visible
progress. Project grouping stays as the alternate mode behind a
one-click sort toggle whose icon reflects the active mode — the old
dropdown cost an extra click for a two-option choice.
Scheduled sessions are detected two ways: the hub-schedule origin
trigger in session metadata, plus a fallback that asks the hub which
session ids belong to schedule executions (list_routine_schedules,
fetched on mount and every two minutes, merged into a rolling set).
The fallback matters because locally executed scheduled runs do not
reliably stamp the trigger into session metadata — a real scheduled
session created today carried only {mode:"user"} provenance. The
scheduled clock icon now leads the row, left of the title; pin and
timestamp stay on the right.
The initial visible page grows from 10 to 30 rows so a tall sidebar
fills instead of stranding a stub of rows over empty space (history
fetches already start at 50).
The expanded sidebar's Customize row now hosts indented Installed and
Marketplace sub-tabs while a customize section is open; the active
sub-tab carries the full selected background while the parent keeps a
subtler one so the two simultaneous highlights read differently.
Also fixes the hover-card flash on click (logo card and session-row
cards): Radix HoverCardContent sits on a DismissableLayer, so a click
on the trigger registers as a pointer-down outside the card and
dismisses it, and the trigger's focus event immediately reopens it.
onPointerDownOutside preventDefault suppresses the dismissal; cards
still close on pointer leave.
* feat(desktop): schedule page row, dialog, and details UX polish
Schedule cards are now click targets: clicking anywhere on a card
outside its controls opens the details dialog (guarded via
closest("button,...") since every inline control, including the Radix
switch, renders a button element), with Enter/Space keyboard support.
The redundant eye button is gone. The remaining edit / run / pause /
delete buttons grow from the 12px icon-sm size to 28px targets with
16px icons, sized consistently with the adjacent enable toggle — the
icons use explicit size-4 classes so the Button base svg rule cannot
shrink them back.
The new/edit dialog gains breathing room between field labels and their
inputs (space-y-2 per field wrapper).
The details dialog no longer scrolls as a whole when the schedule JSON
is long: the dialog is a flex column capped at 85vh, the JSON pre
shrinks to the remaining space (min-h-0) and scrolls internally, and
the Runs tab list scrolls inside the tab the same way.
* feat(desktop): add Retina DMG background tooling
* feat(desktop): customize the macOS DMG layout
* ci(desktop): validate DMG background assets
* fix(desktop): adjust DMG Applications icon position
* ci(desktop): drop redundant DMG artwork validation from publish workflow
Tauri's beforeBuildCommand already runs dmg:background (with its own
validation) at the start of the build/sign/notarize step, and the
release/beta config overlays do not override the build section, so this
step duplicated work the publish job performs anyway. PR-time coverage
lives in desktop-test.yml.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(vscode): stop showing cost estimates for subscription-billed providers
Providers whose usage is covered by a flat-rate subscription (ChatGPT
Plus/Pro via openai-codex, ClinePass) are marked with
metadata.usageCostDisplay = "subscription" in the SDK, and the CLI
already suppresses dollar figures for them. The VS Code host collapsed
that value into "show" before it reached the webview, so the task
header and model pricing rows rendered API-rate cost estimates that
users read as real charges on top of their subscription.
Pass all three usageCostDisplay values ("show" | "hide" |
"subscription") through the catalog listing and render cost only when
the value is "show", matching the CLI's shouldShowCliUsageCost
policy.
* feat(llms): mark Claude Code as a subscription-billed provider
Claude Code is typically authenticated with a Claude Pro/Max
subscription, but its models reuse Anthropic API pricing metadata, so
Cline rendered per-token prices and API-rate cost estimates for usage
that is covered by the subscription. Set usageCostDisplay =
"subscription" on the provider (picked up by the CLI and the VS Code
webview) and suppress the price rows in the Claude Code settings card.
The Claude Code CLI can also run on API-key billing, where a real cost
exists; the provider cannot distinguish the two, so we prefer showing
no number over a misleading one.
* fix(vscode): suppress cost display until provider listings load
While the ListProviders request is in flight (or after it fails), the
usage-cost hook had no listing to consult and fell back to "show",
flashing the API-rate estimate at subscription users on every chat-view
mount — the exact display the previous commit removes. Return
"unknown" whenever listings are absent; consumers already render cost
only for "show", so they suppress it during that window with no
changes. Briefly hiding a real cost is harmless, briefly showing a fake
charge is not.
* capture richer workspace information for vs code extension
* fix(shared): redact credentials from workspace remotes
* fix(shared): avoid regex backtracking in remote redaction
---------
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
The model picker renders a radiogroup of styled buttons with role=radio
and aria-checked; biome's useSemanticElements flags the role as an
error, which fails the sdk-test Quality Checks lint for every PR
touching sdk/ or apps/ paths. Suppress with a justification — switching
to input type=radio needs a restyle and belongs to the desktop settings
work.
* feat(desktop): merge customization pages into a Customize hub with inline marketplace
Replaces the Plugins page and the dedicated Marketplace page with a single
Customize hub. Tabs: Skills, MCP, Plugins, Rules, Hooks, Tools, each with
live counts. Tabs backed by a marketplace catalog render the installed
items followed by an inline browsable Browse section (CLI-hub style), so
installing from the catalog immediately reflects in Installed above.
- Installed cards restyled to mirror the browse-card anatomy: bg-card p-4
containers, absolute top-right xs Uninstall matching Install, truncating
semibold titles, primary-tinted icons, real Badge components instead of
ad-hoc bordered spans, un-indented line-clamped descriptions
- Rules/Hooks/Tools rows brought into the same card language; redundant
intro paragraphs (duplicating the page description) removed; Tools group
headers match the Installed header style with counts
- Marketplace section header renamed to Browse; duplicate 'N results' row
removed (the header count is the single source)
- MCP embedded view now shows the full marketplace instead of
installed-only
* feat(desktop): overhaul sidebar sessions and navigation
Sessions list:
- Sort toggle removed; sessions are always grouped by project, with pinned
sessions leading each group (both subsets ordered by recency). The
Pinned/Scheduled/Tasks category sections and their time-mode paging
machinery (page-fill effect included) are deleted
- Scheduled sessions get an inline clock icon next to the pin position;
pin + clock render together when both apply, and the running/unread
status dot now coexists with them
- One font size (text-sm) across the list: titles, timestamps, project
headers, show-more buttons, empty states. sidebarText needed !text-sm
because the default button size's text-base wins the twMerge conflict
- Gradient fade under the Sessions header once the list scrolls, so rows
fade out instead of hard-clipping
- The session-detail hover card is controlled from the sidebar and closes
on scroll (Radix receives no pointer events while scrolling, so it used
to float over moving content)
- Sidebar min resize width raised 224->260px; the per-project show-more
label truncates so its nowrap text can't force rows to overflow and clip
timestamps at narrow widths
Navigation:
- Customize replaces the Plugins/Marketplace/Hooks/Rules/Tools sidebar
entries; Schedules and Customize are hidden from the expanded settings
nav (their top rows cover them) but stay reachable when collapsed
- The settings gear always opens General instead of resuming the last
section; the Account no-op hover special case is gone
- The New row highlights (aria-current) while the fresh not-yet-started
task page is showing and hands off to the session row once the task
starts; hitting New also focuses the prompt input via a window-event
signal (lib/prompt-input-focus.ts) since the sidebar and composer sit in
distant subtrees
- Fixed the xs button size collapsing any icon-bearing button to 12x12
(leftover has-[>svg]:size-3 from when xs was a micro button) — this was
why Uninstall buttons rendered broken next to Install
* feat(desktop): polish settings pages and chat composer
Models page:
- The provider detail panel is always open: no X button, no empty
no-selection state. It defaults to the first connected provider (falling
back to the first in the catalog), which also removes the layout shift
that happened when the page swapped between full-width and panel
variants on selection
- Fixed the list pane becoming unscrollable while the panel was open:
grid items default to min-size auto, so the pane grew past its track
inside the overflow-hidden grid and its ScrollArea had nothing to
scroll; wrapped it in a min-h-0 min-w-0 cell
- Add Provider opens a Dialog instead of swapping the page
(AddProviderContent gained a dialog variant that renders only the form)
- Embedded inputs (provider search, model search, detail fields) share one
EMBEDDED_INPUT_CLASS stripping the Input component's own border/dark bg
tint/shadow/ring, which rendered as a mismatched inner box; the model
search box uses the same h-9/px-3 frame as the provider search
- Model list flows with the page instead of a max-h capped inner scroller
Other pages:
- Account uses the shared PageFrame/PageHeader: left-aligned, text-3xl
title, Sign Out in the header actions slot
- Desktop notifications is one General section: header row plus the
Event/Notify/Sound matrix nested in a card, so its rows no longer read
as top-level peers of Dark mode; 'Available in the desktop app' label
removed
- Schedule page retitled from Schedules with a real description; Customize
description rewritten
Chat composer:
- The voice dictation button only renders once a voice model is
configured (Settings -> Voice); the unconfigured deep-link state is
gone (prop type kept for an easy restore)
* Redesign desktop Model Providers page and split voice input into its own settings page
- Group providers into Connected / Popular / All with auth-kind hints and
connection status instead of per-row enable toggles
- Show browser sign-in (not an API key field) for OAuth providers, with a
collapsed manual-key escape hatch where supported, plus explicit
Connect / Disconnect / Sign out actions
- Move voice input to a dedicated Settings > Voice page that only offers
connected transcription-capable providers, preselects a default model
(streaming preferred), and stays disabled in the sidebar until a
provider is connected
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Show native tooltip on the disabled Voice settings nav item
Disabled buttons drop pointer events, so the 'connect a model provider'
hint moves to a wrapping span for the browser tooltip to render.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Drop letter avatars and gray provider ids from provider rows and voice chips
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Drop model counts from provider list rows
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Rename provider Connected status to Configured and drop the green styling
A settings entry is configuration, not a live connection; neutral gray
text avoids implying an active link, since the user still picks which
configured provider to use per chat.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Resync provider catalog from disk when a settings save fails
Connect/disconnect/credential edits update the list optimistically; a
failed save now reloads the catalog instead of leaving the optimistic
state (and the view's module cache) claiming a configuration that was
never persisted.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Rename oauthProvider test fixture to dodge CodeQL name heuristic
CodeQL's clear-text-storage query flags any identifier matching 'oauth'
as a credential source and traced the fixture's provider id into the
favorite-models localStorage write, which stores only provider/model id
strings. Renaming the fixture removes the false-positive source.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Guard catalog reloads against races and resync detail drafts on failed saves
Optimistic provider mutations now bump a generation that discards any
in-flight catalog response, so a failed-save recovery reload can't
overwrite a newer edit with an older disk snapshot. The recovery also
remounts the provider detail panel via a reset token so its local field
drafts reflect the reloaded on-disk state instead of unpersisted edits
or an optimistically cleared disconnect.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Fix failed-save recovery ordering and retry superseded reloads
Remount the provider detail only after the authoritative catalog reload
lands, so its drafts re-seed from disk state rather than the optimistic
values that failed to persist. When a concurrent edit supersedes the
recovery's in-flight response, retry the reload (bounded) instead of
dropping it, since that edit performs no reload of its own.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Add Pinned/Scheduled/Tasks categories to desktop app sidebar
Replace the Schedules and Favorites filter-menu options with visible
collapsible category sections in the session sidebar, and rename the
Favorite action to Pin across the sidebar and sessions view.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Grow full history window when Tasks show-more outpaces loaded tasks
loadMoreSessions treats its argument as a limit on all sessions, but the
Tasks show-more count only tracks Task rows, so once pinned/scheduled
rows pushed the loaded total past the requested count the call no-oped
and clicks went dead. Grow the whole history window via
loadOlderSessions instead, and only when the loaded tasks cannot fill
the next page. Addresses Greptile review on #13528.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Auto-fill the Tasks page instead of fetching once per show-more click
A single 50-session window growth can consist entirely of pinned or
scheduled sessions, leaving a show-more click with no visible Tasks
progress. Replace the one-shot fetch with a page-fill effect that keeps
growing the history window until the requested Tasks page fills or
history runs out. Addresses the follow-up Greptile review on #13528.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Halt page-fill retries after a failed history fetch
A failed fetch leaves the task count and has-more state unchanged,
which are exactly the conditions the page-fill effect fires on, so one
failing request would retry and re-toast forever. Halt the effect after
a failure and let the next explicit show-more click retry. Addresses
the third Greptile review on #13528.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: clean up sidebar navigation chrome
- Give New Task its own full-width labeled row below the logo row
instead of an ambiguous icon next to the agenda toggle
- Wire the New Task row to the home action so starting a new task
clearly takes you home (the logo still works as a fallback)
- Swap back/forward chevrons for browser-style arrow icons and
bump their size
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: sidebar New/Schedule/Customize rows and always-visible search
- Stack New (plus icon), Schedule, and Customize as full-width labeled
rows below the logo; whole row highlights on hover via sidebarItem
- New starts a fresh task (home), Schedule opens Settings > Schedules,
Customize opens the Customizations sections (Plugins first)
- Show the session search bar permanently above the sessions list
instead of hiding it behind a search icon toggle
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: move session search into a dialog behind a logo-row icon
- Replace the inline sidebar search bar with a search icon in the
logo row that opens a cmdk command dialog listing sessions
- Selecting a result opens that session and closes the dialog
- Remove the agenda/tasks toggle the icon replaces, along with the
now-unreachable sidebar Agenda panel (the welcome screen still
surfaces agenda tasks)
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: load full session history when the search dialog opens
Addresses Greptile review on #13533: the dialog only searched the
currently loaded history batch, so older unloaded sessions could not
be found. Opening search now kicks off loadAllSessions() (the hook's
purpose-built global-search loader), and the empty state reads
'Searching older sessions...' while more history is streaming in.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* chore(vscode): remove per-tool MCP auto-approve checkboxes from webview
MCP auto-approval is now governed solely by the global 'Use MCP servers'
toggle; the SDK approval path (shared with the CLI and desktop app) has no
per-tool granularity, so the per-tool and 'Auto-approve all tools'
checkboxes were no-ops that implied control that no longer exists. Remove
them from the MCP settings view and chat tool rows. The autoApprove arrays
in cline_mcp_settings.json and the toggleToolAutoApprove RPC are left
intact for the legacy extension.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* chore(vscode): hide per-tool MCP auto-approve checkboxes behind a flag
Keep the checkbox components, handlers, and RPC plumbing intact but gate
rendering behind SHOW_MCP_PER_TOOL_AUTO_APPROVE=false: the SDK approval
path (shared with the CLI and desktop app) is all-or-nothing via the
global 'Use MCP servers' toggle, so the per-tool checkboxes were no-ops.
Flip the flag back on if the SDK gains per-tool approval granularity.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(core): prevent search_codebase from crashing the process on giant single-line files
searchWithRipgrep buffered all of rg's --json stdout into one string. Each
JSON event embeds the full text of the matched line (--max-columns is
ignored in JSON mode), so searching a directory of serialized trace dumps
(single-line multi-hundred-MB JSON files) accumulated gigabytes of stdout
until string concatenation threw RangeError: Out of memory inside the
stream data handler. That throw is outside the tool's try/catch, so it
escalated to an uncaughtException and killed the CLI/hub daemon.
Parse rg's JSON events incrementally line by line, drop events larger
than 256KB, truncate matched/context lines to MAX_LINE_CHARS, and stop
reading once maxResults is reached. The fallback regex scan now skips
files larger than 10MB (reporting the skip count) and truncates its
context lines the same way.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* simplify search_codebase crash fix to a minimal diff
Replace the incremental JSON-event parser with three small guards: stop
buffering rg stdout past 10MB, drop the trailing partial event before
parsing, and slice fallback context lines to MAX_LINE_CHARS. Drops the
fallback file-size skip and skip-count reporting.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
The /shutdown handler queued teardown on a microtask, which runs before
the event loop's write phase, so the daemon could process.exit() before
the accepted 202 was handed to the socket. Unix masked it (uv_try_write
lands small loopback writes synchronously); Windows has no such fast
path and lost the race regularly — the recurring shutdown.e2e.test.ts
'socket hang up' failures on windows-latest. Start teardown from the
response's write callback instead, with an idempotent 1s fallback so a
client that vanishes mid-write cannot strand the daemon, and send
Connection: close so the client gets a FIN rather than an abort.
Since the flakiness this compensated for is fixed at the source, restore
maxWorkers: 2 for the Windows core suite (serializing it cost ~3 min of
CI per run), and raise the e2e daemon discovery hang guard 10s→30s —
it guards against hangs, not runner speed.
ci-node-smoke.ts installs the packed SDK tarballs with a plain npm
install in a fresh temp dir, where the repo root package.json overrides
do not apply. When @sap-cloud-sdk 4.9.0 shipped (2026-08-24) it broke
@sap-ai-sdk/ai-api 2.14.0 (via @jerome-benoit/sap-ai-provider in
@cline/llms) with ERR_PACKAGE_PATH_NOT_EXPORTED, failing the smoke step
on every PR even though the root already pins @sap-cloud-sdk/* to 4.6.0.
Copy the root overrides block into the generated sandbox package.json
so the smoke install resolves the same pinned versions as the repo and
future third-party releases cannot break it independently.
* fix(hub): cap hub-events db size so it can't fill the disk
Row/time retention alone didn't bound disk usage: envelopes carrying
full session snapshots reach hundreds of KB each, so retained rows
could total tens of GB, sweeps only ran hourly, and DELETE never
shrinks a SQLite file. Enforce a 64 MiB size budget in prune() (oldest
rows first, VACUUM to return the space), and also prune after every
16 MiB appended so bursts can't outrun the hourly timer.
Fixes#13505
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(hub): tolerate VACUUM failure on a full disk
VACUUM needs scratch space and can fail in exactly the state a
ballooned event log causes. The byte-budget deletes already bound live
data, so swallow the error and let the next sweep retry the reclaim
instead of aborting startup pruning and disabling the durable log.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(hub): count the size budget in UTF-8 bytes, not characters
envelopeJson.length (UTF-16 units) and SQLite LENGTH() (characters)
undercount multibyte text by up to 3x, which could leave a CJK-heavy
log settled above budget and re-running VACUUM every sweep. Use
Buffer.byteLength and LENGTH(CAST(... AS BLOB)) instead.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(vscode): resolve hook workspace identity from the window, not shared global state
Hook discovery, hook cwd selection, and the workspaceRoots metadata passed
to hook scripts all read the workspaceRoots/primaryRootIndex global state
keys. Global state lives in ~/.cline and is shared by every Cline instance
(all VS Code windows, the CLI, the JetBrains plugin), and nothing writes
these keys anymore, so hooks resolved against whatever project some other
or older instance last recorded. With a second window open on another
project, a workspace's .clinerules/hooks scripts were never discovered.
Resolve workspace roots via a single guarded helper backed by
HostProvider.workspace.getWorkspacePaths() (in-process, window-scoped,
same as refreshHooks): blank paths are filtered, a host-bridge failure
degrades to no workspace roots instead of silently disabling global hooks
or skipping blocking PreToolUse guards, and one resolution is threaded
through hooks-dir discovery, cache misses, cwd selection, and hook input
metadata so they can't disagree (previously up to four host lookups per
hook execution — real gRPC round trips in the standalone host). Roots and
hooks dirs are matched on whole path segments with the longest root
winning, so prefix-sharing or nested workspace roots resolve to the right
project. The adapter creates the runner once per event and skips no-op
runners, making creation the single resolution point; the separate
hasHook/getHookInfo checks are removed. The dead workspaceRoots and
primaryRootIndex state keys are dropped, and the four hand-rolled
HostProvider.workspace test stubs are consolidated into one shared
helper.
* test(vscode): add e2e coverage for workspace-scoped hook discovery
Boots real VS Code with the packaged extension against the workspace
fixture, sends a prompt, and asserts the fixture's UserPromptSubmit hook
was discovered from the open window's workspace, executed with that
workspace root as its cwd, and received the same root in its
workspaceRoots input — the end-to-end contract the hook workspace
identity fix establishes.
* test(vscode): isolate the e2e hook fixture from the shared workspace
The UserPromptSubmit fixture hook lived in the shared e2e workspace, so
every prompt-sending spec executed it (hooksEnabled defaults to true) —
and its cold PowerShell spawn on Windows pushed chat.test.ts past the
5s expect timeout. hooks.test.ts now overrides workspaceDir to a
dedicated workspace-hooks fixture, so only the hooks spec pays the hook
spawn.
The $4.99 first-month promo is ending, so the CLI's first-launch "Try ClinePass" dialog should no longer advertise it. Also drops the leftover CLI_PROMO_CODE plumbing, which has been an empty string since the promo-code flow was removed.
* fix(vscode): honor MCP auto-approve settings for SDK tool calls
The SDK extension required both the global 'Use MCP servers' auto-approve
toggle AND each tool's per-tool autoApprove flag before silently approving
an MCP call, while the legacy extension treated them as either/or. Restore
the legacy OR semantics so toggling MCP auto-approve works again.
Also key toolPolicies by the registered SDK tool name (via
defaultMcpToolNameTransform, now exported from @cline/core) instead of raw
server__tool. Servers whose names contain sanitized characters (e.g.
marketplace names like github.com/user/repo) or exceed 64 chars produced
policy keys that never matched the registered tool, so those MCP tools ran
without any approval gate; the live auto-approve lookup now re-applies the
transform instead of string-splitting the name.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Revert "fix(vscode): honor MCP auto-approve settings for SDK tool calls"
This reverts commit 86c568fbba.
* fix(vscode): auto-approve all MCP tool calls when the MCP toggle is on
The SDK extension only auto-approved an MCP call when the global 'Use MCP
servers' auto-approve toggle AND that tool's per-tool autoApprove flag were
both set, so toggling MCP auto-approve appeared to do nothing and users had
to opt in each tool individually. The toggle alone now governs all MCP
tools; the per-tool flag is no longer consulted.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
The task.completed fallback lived only inside shutdownSession, but
stopSession/dispose route interactive sessions with a terminal reported
status through releaseSessionRuntime, which never emitted. Truthful
session-status reporting (shipped in 4.1.11) re-routed a large share of
interactive stops onto that branch and silently dropped the event.
Route the emission through a single choke point,
emitTaskCompletedOnTeardown, called from both shutdownSession and
releaseSessionRuntime. The completion criterion no longer reads
session.status: interactive sessions use the recorded final-turn
outcome (lastInteractiveTurnFinishReason), non-interactive sessions
keep the existing input.status === "completed" logic. A new
taskCompletedEmitted flag (also set by the submit_and_exit observer)
enforces exactly one task.completed per session. failSession now
records the errored final turn so a stale "completed" from an earlier
turn can never leak into the teardown emission. Telemetry only; no
user-facing behavior changes.
Four consecutive SDK publish runs failed on windows-latest, each on a
different test, all of them plain timeouts: two @cline/shared SQLite
tests at the 5s vitest default, core's bash executor at 10s, and the hub
singleton endpoint test at 10s. The 2-core Windows runner spawns forks
and takes SQLite locks slowly enough to blow those budgets under load.
These timeouts guard against hangs; they are not timing assertions (the
one suite that does assert elapsed time, shutdown.e2e, was fixed by
removing file-level parallelism instead). Raise core to 20s and give
@cline/shared an explicit 15s in place of the inherited 5s default.
singleton.e2e.test.ts (added in #13468) spawns real daemons and runs for
~15s. Vitest's default file parallelism let it run alongside
shutdown.e2e.test.ts, whose assertions are wall-clock bound: discovery
within 10s, exit within 5s, and a 2s shutdown watchdog. On the 2-core
windows-latest runner that contention alone broke those budgets, failing
the shutdown test two different ways across runs — once never observing
discovery, once with the daemon forced to exit before its HTTP 202
flushed (socket hang up). The test passed on Windows before #13468 and
has failed every SDK publish run since.
* fix(core): seed tools capability when custom model capabilities are synthesized from boolean flags
For a models.json entry with no explicit capabilities list, toStoredModelInfo
synthesized a capability array purely from boolean convenience flags (e.g.
supportsReasoning: true -> ["reasoning"]). modelSupportsToolCalling fails open
only for a missing or empty list, so the synthesized non-empty list read as an
authoritative denial and silently stripped every tool definition from requests
to custom OpenAI-compatible models (#13463).
Seed "tools" whenever the list was not explicitly authored and the boolean
projections made it non-empty, preserving the fail-open contract. Explicitly
authored capability lists remain authoritative and can still disable tools.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* test(core): cover stale catalog capability overrides
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix: treat stored capability lists as non-authoritative for tool calling
The hasExplicitCapabilities guard still let two producers of tool-less
lists through:
- The VS Code legacy-override migration (legacyModelInfoToOverrides)
persists explicit partial lists like ["prompt-cache"] into models.json
for custom OpenAI-compatible models, which then read as an authoritative
"cannot call tools" and drop every tool - same symptom as #13463.
- Any hand- or UI-authored partial list on a non-catalog model.
Stored entries and user-authored provider metadata have no way to declare
"cannot call tools" (there is no supportsTools field, and every writer
that authors a full list includes "tools"), so seed "tools" into any
non-empty list for a language model. Only generated catalog capabilities
remain authoritative - a genuine no-tools catalog model stays that way -
and non-language models (e.g. image generation) never gain a tools claim.
Also make legacyModelInfoToOverrides write "tools" into the arrays it
fabricates, matching the providers.json migration, so models.json stops
being poisoned for older readers.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* feat(hub): add drain and upgrade commands with replay support
* handles disconnection
* feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport
Completes the wiring the previous commits' primitives needed:
HubServerTransport gains isDraining(), hub.drain/hub.status/profile.get
command handling, and replayEventsAfter() (backed by the durable event
log), plus the sequence/sinceSequence wire types they depend on in
shared/hub.ts. run-queue-handlers.ts reads the active bot profile's
plugin roots when executing durable runs.
Also adds hub/profiles/: profile.json (identity/rules/plugins) ->
system prompt composition, --profile / CLINE_HUB_BOT_PROFILE
resolution, and the bundled cline-dad profile with its
cline_hub_support read-only diagnostics tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Revert "feat(hub): wire bot profiles, drain, and durable event/run-queue into the live transport"
This reverts commit 6696d5d202.
* fix(hub): dedupe replayed events by eventId, not just sequence
HubEventLogStore.append() returns a new envelope stamped with a
sequence rather than mutating the input, so a pending approval
re-issued sequence-less by subscribe() (it predates any durable-log
append) and its later sequence-stamped copy from the durable log are
two different objects carrying the same eventId. The replay-then-live
buffer in browser-websocket.ts only deduped by sequence, so the
sequence-less copy's guard never tripped and it was delivered a second
time when the buffer flushed after replay.
Track delivered eventIds alongside the sequence cursor; eventId
survives the append/stamp round-trip unchanged, so this dedupes the
exact-same logical event regardless of which copy arrives first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(hub): wire drain, durable event log, and run queue into the live transport
CI on this branch failed bun run build:sdk: browser-websocket.ts,
client/index.ts, and hub-websocket-server.ts (already on this branch)
reference sequence/sinceSequence, HubServerTransport.isDraining(), and
the "hub.drain" command — but the commit that reverted bot profiles
out of this branch also reverted this wiring, since it shared a commit
with the profiles work. That wiring is a hub concern, not a
bot-profiles one; split it back out.
- shared/hub.ts: sequence/sinceSequence types, run.enqueue/run.list/
hub.drain/hub.status/stream.replay capability, command, and event
names. profile.get intentionally excluded — stays bot-profiles-only.
- context.ts: isDraining() on HubTransportContext. botProfile field
intentionally excluded.
- hub-server-transport.ts: eventLog/runQueue fields and start/stop
lifecycle, publish() appends to the durable log, handleCommand cases
for run.enqueue/run.list/hub.drain/hub.status, drain-refusal check,
replayEventsAfter()/lastEventSequence(). startBotProfile()/
startHubSupportTool() and the profile.get case intentionally
excluded.
- run-queue-handlers.ts: added without handleProfileGet (needs
ctx.botProfile, which doesn't exist here).
- hub-upgrades.test.ts: added without its two bot-profile-injection
tests (they need a resolved bot profile to assert against).
Verified bun run build:sdk exits 0 (the exact CI command) and
bunx vitest run src/hub passes (311/312; the one failure is the
same pre-existing environment-timing flake already present before
this change).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(hub): export instance-lock, event-log, and run-queue from the hub barrel
These landed as internal modules only; hub-server-transport.ts and
hub-websocket-server.ts import them by direct path, but nothing
re-exported them from the public @cline/core/hub surface the way
sibling discovery/server modules already are.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(hub): wire the instance lock into the daemon entry point
The singleton lock (discovery/instance-lock.ts) and its consumption in
startHubWebSocketServer/ensureHubWebSocketServer were already on this
branch, but the daemon entry point's own half was not: retrying a bind
when a retiring predecessor still holds the lock, and exiting with a
distinct code (3) instead of the generic fatal path when a live Hub
already owns the data directory. Without this, a daemon racing a
retiring predecessor could fail outright instead of waiting the lock
out, and losing the singleton race looked identical to a crash.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(hub): address drain/upgrade review findings (#13478)
- cline hub upgrade: check idleness at least once (--wait 0 works), reject
non-numeric --wait, and un-drain on every abort path so an aborted
upgrade can never leave the hub refusing new work
- add cline hub drain --off and the off query param to requestHubDrain so
POST /drain?off is reachable from shipped code
- HubEventLogStore/HubRunQueue: WAL journal mode + busy_timeout, and stamp
sequences from lastInsertRowid instead of SELECT MAX(sequence)
- HubInstanceLock.acquire: degrade to an unheld lock when SQLite is
unavailable instead of refusing hub startup; only BUSY/LOCKED still
raises HubLockHeldError
- ensureHubWebSocketServer: retire an unusable discovered hub through the
shared retireDiscoveredHub (busy hubs are attached to, drain precedes
shutdown, discovery cleared only when the hub actually retired)
- replay adapter: advance the cursor past eventId-deduped events, cap
replay pages, stop when the cursor stalls, and drop the dedupe set after
the buffered flush so it cannot grow for the socket lifetime
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* fix(hub): derive the singleton e2e challenger cwd portably
The challenger's working directory was derived by round-tripping the
discovery path through a file: URL and stripping the last pathname
segment. On Windows that yields a POSIX-style '/C:/...' path, which is
not a valid spawn cwd, so the spawn fails ENOENT before the singleton
lock is ever contested and the Windows SDK test job goes red.
The data dir is simply the discovery file's parent: use dirname().
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
The PublishNightly environment gained required reviewers, so each cron
run parked on approval, held the workflow's concurrency group, and
silently cancelled every scheduled run queued behind it. 20 consecutive
scheduled nightlies died this way between 2026-07-31 and 2026-08-21;
the only nightlies that shipped in that window were manual dispatches.
Drop the cron rather than leave a trigger that cannot succeed unattended.
* fix telemetry session propagation
* feat telemetry client version metadata
* fix(core): address Langfuse review feedback — hub client identity + delegated agent session grouping (#13475)
* fix(core): rebuild hub session client identity from request headers
Hub-backed sessions do not transport extensionContext (it is local-only),
so the daemon's runtime built traces without the clientName/clientVersion
metadata even though the hub client bakes X-CLIENT-TYPE / X-CLIENT-VERSION
into the session's provider headers. Reconstruct extensionContext.client
from those headers during local runtime bootstrap so hub-backed Langfuse
traces carry the same client identity as local runtimes, and the daemon's
header re-resolution stops clobbering the original X-CLIENT-TYPE.
* fix(core): propagate parent distinctId/sessionId to delegated agents
Delegated agents (spawned sub-agents, configured agents, teammates) were
built without distinctId and sessionId, so their Langfuse traces had no
userId or sessionId and did not group with the parent user or session.
Thread the host-resolved distinctId through RuntimeBuilderInput and the
root sessionId through the delegated-agent config provider, and copy both
onto the delegated AgentConfig.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>