Commit Graph
7269 Commits
Author SHA1 Message Date
Saoud Rizwan a0a8c060fb fix(desktop): clear legacy Codex credentials on ChatGPT sign-out (#14040)
* fix(core): import each legacy provider only once so sign-out is not undone

Every ProviderSettingsManager construction re-ran the legacy
globalState.json/secrets.json import and re-added any provider missing
from providers.json. Removing a provider (e.g. signing out of the
ChatGPT/Codex provider in the desktop app) deleted its entry, so the
next sidecar command immediately re-imported the credentials from
secrets.json and the user appeared signed in again.

Track imported provider ids in providers.json (migratedLegacyProviders)
and skip them on later runs. Providers that already had an entry are
recorded too, so removing them later also sticks. New legacy providers
that appear afterwards are still imported, so the classic-extension
handoff keeps working.

* Revert "fix(core): import each legacy provider only once so sign-out is not undone"

This reverts commit db013c8352.

* fix(desktop): clear legacy Codex credentials on ChatGPT sign-out

Signing out of the ChatGPT (openai-codex) provider removes its
providers.json entry, but ProviderSettingsManager re-imports missing
providers from the legacy extension's secrets.json on every
construction, so the next sidecar command signed the user back in.

Remove openai-codex-oauth-credentials from secrets.json when the
desktop app signs out of that provider. Temporary until the legacy
import is retired.

* fix(desktop): surface failed legacy secrets.json write on ChatGPT sign-out

A failed write now throws so the webview reports the sign-out as failed
and resyncs, instead of reporting success and being signed back in by
the next legacy import. Missing or unparseable files stay a no-op.
2026-09-10 17:06:42 -07:00
BeeandSaoud Rizwan 31b0dd9900 fix(desktop): refresh live model catalogs for all shared providers CLINE-3239 (#14003)
* fix(desktop): refresh live model catalogs for all shared providers

* fix: exclude private catalogs from shared refresh and measure model loads

* fix: redact registered provider IDs in model telemetry

* refactor: drop provider.models_loaded telemetry from live catalog fix

Keep this PR scoped to the model refresh bug. The event counted cache hits
and bundled fallbacks as 'returned', so it could not tell whether a live
refresh actually succeeded; split it out for a dedicated follow-up if
model-load observability is still wanted.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-09-10 14:26:19 -07:00
BeeandSaoud Rizwan ea368a1fc5 fix(automation): make event acceptance atomic and retryable (#14039)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-09-10 14:24:04 -07:00
alex-lum f6359842a5 fix(llms): limit Langfuse telemetry to Cline backend providers (#13851)
* fix(llms): limit Langfuse telemetry to Cline provider

Refs ENG-2505

* fix(llms): include ClinePass in Langfuse telemetry

Refs ENG-2505
2026-09-10 14:08:06 -07:00
Saoud Rizwan a35f9c7e35 fix(desktop): point local-auth credential failures at the provider CLI (#14035)
* fix(desktop): point local-auth credential failures at the provider CLI

When a Claude Code / Codex CLI / OpenCode turn fails with an auth error
(e.g. Claude Code's 'Failed to authenticate: OAuth session expired and
could not be refreshed'), the failure hint sent users to Settings ->
Models, where there is nothing to fix: the login lives in the CLI on the
machine. Resolve the provider's local CLI from the catalog and tell the
user to sign in again there instead. Also classify 'authenticate' and
'session expired' as credential failures so that exact error gets a hint
at all.

* fix(desktop): treat 'not logged in' / '/login' turn failures as credential errors

Claude Code reports a signed-out CLI as 'Not logged in · Please run
/login', which the credential classifier missed, so the hint never showed.
2026-09-10 13:55:08 -07:00
BeeandSaoud Rizwan 05dbe644d1 fix(llms): report zero cost for Cline Pass and free models (#14012)
* fix(llms): report zero cost for Cline Pass and free models

* fix(llms): refresh included pricing and track cost corrections

* fix(telemetry): catalog included cost corrections with typed capture

* refactor(llms): drop included-cost correction telemetry

The sdk.cline_included_cost_corrected event fired on essentially every
Cline Pass / free response (the API reports upstream cost on those), so
it carried no signal, and computing the unadjusted cost for it ran
calculateUsageCostFromPricing on every usage event for every provider.
Keep the zero-cost fix in normalizeUsage and remove the shared helper,
core event registration, and telemetry plumbing.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-09-10 13:27:20 -07:00
Bee 7877d0e0a3 Unify Tools, Skills, and Rules tab styling in desktop Customize view (#13837)
* feat(desktop): unify Tools, Skills, and Rules tab styling in Customize

Brings the Tools, Skills, and Rules tabs in the desktop app's Customize
view in line with the pattern Plugins already used, so every tab now
reads as one consistent list style instead of three different ones:

- Tools: section titles now read "BuiltIn Tools <count>" / "Plugin
  Tools <count>" with the count in muted text, add a search bar that
  filters both sections, and add an Enable all/Disable all checkbox
  per section that only touches the currently filtered/visible tools.
- Skills: replace the standalone destructive Uninstall button with the
  same dots menu (Copy path / Uninstall) Plugins use, and add a toggle
  switch to enable/disable a skill in place. This required backend
  wiring since skills previously had no enabled/disabled concept in
  the desktop app: a new set_skill_disabled sidecar command calls the
  existing hub settings.toggle({type: "skills"}) path (the same one
  the CLI's TUI config panel already uses), and disabled skills are no
  longer filtered out of the listing so they can be found and
  re-enabled.
- Workflows: show the same toggle switch as skills for visual
  consistency, but disabled with a tooltip, since core's settings
  toggle doesn't have a "workflows" branch yet.
- Rules: add the same dots menu (Copy path only, since rule deletion
  isn't wired up in uninstallLocalPrimitive) and a disabled toggle
  switch, and drop the raw path text now that it's reachable via Copy
  path, matching how Skills/Plugins cards are laid out.

Also included: reorder the Customize tabs to Tools, Plugins, Skills,
Rules, MCP, Hooks and default to the Tools tab; add a "muted" badge
variant used by the Plugin Tools badge; rename the sidebar "New Task"
button to "New Session".

* fix(desktop): stop bulk tool toggle fallback from mis-flipping grouped tools

The legacy toggle_disabled_plugin_tool fallback (used only when a
sidecar predates set_tool_disabled) flips one name with no explicit
target state. That only reliably lands on the desired state when a
tool maps to a single underlying name — a tool with several headless
names in a mixed disabled state can end up unchanged or wrong, since
blindly flipping every name can move it further from the goal instead
of closer. Skip those tools in the fallback instead of silently
leaving them wrong, apply whatever did succeed, and surface a clear
error naming what couldn't be changed. Same fix applied to both the
single-tool and bulk enable/disable paths.

* fix(desktop): let the legacy tool-toggle fallback flip uniform groups

The previous fix blocked every multi-name tool from the legacy
toggle_disabled_plugin_tool fallback, even ones where the underlying
names shared the same prior state and could be safely flipped as a
group. Instead, attempt the group flip and verify the tool's resulting
state against the target: if it matches (a uniform group), keep it; if
it doesn't (a mixed group the legacy command can't reliably resolve),
flip everyone back to restore the original state and report that tool
as needing a newer build, rather than leaving it in some other
unintended combination.

* fix(desktop): make the legacy tool-toggle fallback direction-aware

The previous fix's verification used the tool's aggregate enabled flag
to confirm a grouped flip landed on target, but that flag is true only
when every underlying name is enabled — it can't tell "now fully
disabled" apart from "still mixed," since both report false. So the
disable direction could report success while some underlying actions
stayed enabled.

Extract the group-flip logic into toggleLegacyToolGroup, shared by the
single-tool and bulk paths, and make it direction-aware: enabling
verifies with one flip (the aggregate flag is an exact test for that
direction), disabling verifies with a second probe flip whose result
is the exact test for "did the first flip fully disable it," redoing
the first flip if so. Either direction, on failure, flips back to the
exact original state rather than leaving some other combination.

* fix(desktop): use explicit tool states without legacy fallbacks
2026-09-10 13:17:50 -07:00
Bee 4063e1731e fix(desktop): reject unsupported image attachments CLINE-3234 (#14000)
* fix(desktop): reject unsupported image attachments

* fix(desktop): validate draft images and track blocked attachments

* fix(desktop): reject unsupported formats and catalog image telemetry

* fix(desktop): disable unsupported attachments and validate dropped images

* fix(desktop): separate image attachments from file picker

* refactor(desktop): simplify image attachment validation
2026-09-10 12:11:28 -07:00
BeeandSaoud Rizwan 75ac98a3c8 feat(desktop): add PR opening and merge/CI status indicators CLINE-3226 (#13997)
* feat(desktop): add pull request links and merge/CI status

* fix(desktop): align PR status colors with merge readiness

* feat(desktop): track pull request feature interactions

* fix(desktop): use camelCase PR telemetry properties

* fix(desktop): hide unavailable PR integration and throttle retries

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-09-10 10:48:55 -07:00
BeeandSaoud Rizwan 258227dbe0 fix(desktop): prevent history hover metadata overlap (#14005)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-09-10 10:07:24 -07:00
Bee fc3273bbe0 refactor(hub): share describeOutdatedHubSessions between CLI and desktop (#13772)
* refactor(hub): share describeOutdatedHubSessions between CLI and desktop

Post-merge review follow-up from #13727: the helper existed word for word
in the CLI TUI and the desktop webview, with tests only on the desktop
copy. Both surfaces must read identically, so the copy now lives once in
@cline/shared (exported on both the node and browser surfaces, which the
webview already imports) with the test suite moved alongside it.

* fix(desktop): use browser shared entry for hub update dialog
2026-09-10 16:10:19 +09:00
avianion ae0c86f11f docs: add Avian as OpenAI-compatible provider guide (#9677)
* docs: move Avian to consolidated providers page

* Fix alphabetical ordering of provider entries
2026-09-10 15:35:36 +09:00
46ba1d3228 fix(shared): preserve nested PowerShell scripts across editions (#13815)
* fix(shared): unwrap nested PowerShell -Command before the wrapper parses it

run_commands feeds commands to PowerShell through a stdin bootstrap that
executes the text as outer PowerShell source. A nested
`powershell -Command "... $_ ..."` therefore had its double-quoted
argument parsed by the outer parser: $_ was interpolated away before the
nested shell ever saw it, so pipelines like
`... | Where-Object { $_.Name ... }` errored once per enumerated item -
an error flood over large trees that looked like a hang - while the
nested child still exited 0.

Detect redundant nested invocations in getShellInvocation and run the
decoded script directly. Only semantics-preserving rewritings are done:
same PowerShell edition as the configured outer shell, -NoProfile plus
only wrapper flags the bootstrap already applies (a nested shell that
would load the user's profile keeps its own process), and an entirely
double-quoted -Command tail. Escapes decode per PowerShell rules,
including the PowerShell 7-only `u{...} and `e. Everything else passes
through byte-identical.

Fixes #13284

* fix(shared): preserve nested PowerShell statement boundaries

* fix(shared): preserve requested PowerShell across nested editions

* test(core): compare canonical PowerShell fixture paths

* fix(shared): keep nested PowerShell helper internal

* fix(shared): unwrap single-quoted PowerShell scripts

* fix(shared): require call operator for quoted PowerShell paths

* refactor(shared): drop the deprecated unwrapNestedPowerShellCommand helper

It had no production callers and existed only so tests could import it.
Move its explanation onto parseNestedPowerShellCommand, which is what
getShellInvocation actually runs, and assert on getShellInvocation's
executable and input in the tests instead.

---------

Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-09-10 13:44:00 +09:00
Saoud RizwanandClaude Opus 5 35a0597471 chore(desktop): release v0.0.25
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJ3f53RvXh2TBHghfEU7UK
desktop-v0.0.25
2026-09-09 21:37:08 -07:00
Saoud RizwanandSaoud Rizwan c393958c52 desktop: give session import its own Settings page (#14023)
* desktop: surface session import as a notice on the welcome screen

The import-from-other-agents flow only lived in Settings, the Sessions page
header, and the onboarding step, so existing users rarely discovered it.
Show a dismissable notice above the composer on the new-chat screen when
un-imported Claude Code / Codex / opencode history exists on the machine.

The scan runs once per app run and is skipped while onboarding is showing
(it has its own import step); onboarding's skip/import records the same
dismissal so the welcome screen does not repeat the offer.

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

* desktop: singular-aware copy in the welcome import notice

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

* desktop: make the welcome import notice a low-key footnote under the composer

The card-style banner took too much space on the welcome screen. Render it
as a single muted line below the composer with an inline Import link and a
dismiss icon, via a new footnote slot on WelcomeScreen.

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

* desktop: drop the welcome-screen import hint in favor of a Settings page

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

* desktop: give session import its own Settings page

The import row was easy to miss in General. Add an Import section to the
settings nav with a page that shows what Claude Code, Codex, and opencode
history the scan finds on this machine and opens the existing import
dialog. Remove the row from General.

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

* desktop: only let the latest import scan update the Import page counts

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

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-09 21:28:09 -07:00
BeeandSaoud Rizwan be65a78b38 fix(sdk): generate offline Cline recommended, free, and subscribed lists (#14015)
* fix(sdk): generate offline Cline featured model lists

* fix(sdk): stage catalog outputs and verify every featured model

* fix(sdk): register recommendation telemetry in the event catalog

* refactor(sdk): trim model catalog generation to focused changes

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-09-09 21:01:28 -07:00
Saoud Rizwan c8a0ace8b6 test(core): widen exit-grace shell test budget for slow Windows runners (#14022)
The two inherited-stdio exit-grace tests gave Git Bash a 5s command
timeout. A cold Git Bash start on the 2-core windows-latest runner can
take several seconds, so the timeout fired before the exit-grace timer
ever ran, failing sdk-test intermittently on Windows.

Raise the command budget to 15s (still under Vitest's 20s testTimeout)
and lengthen the background sleep so it keeps outliving the timeout.
The tests still fail with TimeoutError when the exit-grace path is
disabled.
2026-09-09 20:51:53 -07:00
Saoud RizwanandSaoud Rizwan 3033c394c7 desktop: mark configured providers in the composer provider picker (#14021)
* desktop: mark configured providers in the composer provider picker

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

* desktop: show the configured indicator only on provider rows, not the trigger

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

* desktop: refresh provider readiness on catalog invalidation; fix composer test fixtures

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

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-09 20:49:55 -07:00
Saoud RizwanandSaoud Rizwan 2be718baba fix(llms): mark OpenCode provider as local-auth instead of oauth (#14018)
* fix(llms): tag OpenCode provider as local-auth instead of oauth

The OpenCode SDK provider spawns the local opencode server, which
authenticates from the credentials opencode's own CLI stores. Cline has
no OAuth handler for it, so the "oauth" capability made the desktop app
render a browser sign-in button that could not do anything. Declare it
local-auth with its CLI command and docs URL so hosts show the local CLI
notice (like Codex CLI / Claude Code) and can probe for the executable.

Fixes CLINE-3236

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

* chore(llms): tighten OpenCode local-auth comment

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

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-09 20:14:33 -07:00
Dominic Cooney 97a0d27d16 test(llms): isolate Bedrock fallback cases from catalog updates (#14017) 2026-09-10 11:50:28 +09:00
Saoud RizwanandSaoud Rizwan c52e87d52d fix: Codex subscription model list and picker (#14011)
* fix(core): limit ChatGPT subscription known models to the Codex catalog

toProviderConfig filled knownModels for openai-codex with the raw
openai-native catalog it shares, so the desktop/hub model picker
(list_provider_models) overwrote the filtered Codex list with the full
OpenAI API catalog (gpt-4o, gpt-4.1, chatgpt-image-latest, ...), and the
runtime handler lost the Codex context caps. Apply filterOpenAICodexModels
when building the config-derived catalog.

Fixes CLINE-3232

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

* fix(llms): align ChatGPT subscription model rules with Codex

Mirror opencode's current ChatGPT-plan rules for the shared OpenAI catalog:

- Drop gpt-5.4 and gpt-5.4-mini: OpenAI retired them for ChatGPT accounts
  in Codex on 2026-08-31 and the backend now rejects them.
- Move the provider default from gpt-5.4 to gpt-5.6-terra, OpenAI's stated
  replacement. Keeping a retired default would also re-inject it into the
  list as a fallback model.
- Compare GPT versions by major/minor so integer versions (gpt-6-astra)
  and multi-digit minors are not dropped by parseFloat.
- Allow gpt-5.3-codex-spark explicitly; deny gpt-5.5-pro and the bare
  gpt-5.6 alias of the Sol variant.
- Cap every Codex model at the 400K / 272K / 128K backend budget instead
  of only gpt-5.5, so GPT-5.6+ no longer inherits the API's 1.05M limits.

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

* refactor(llms): own the Codex catalog view in @cline/llms

Move the ChatGPT-subscription special case out of core's generic
toProviderConfig into a getGeneratedModelsForRuntimeProvider helper next to
buildOpenAICodexModels, so core just asks llms for the catalog a runtime
provider reads from.

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

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-09 18:55:17 -07:00
Saoud RizwanandSaoud Rizwan 35b4cd010a desktop: preserve the prompt when the provider connection fails (#14008)
* desktop: hand the prompt back to the composer when the runtime never takes it

When a send fails before the turn begins (e.g. switching to Codex and the
OAuth refresh throws), the sidecar synthesizes a messages-less error result
and the user turn was never appended to the session. Post-send hydration
then wiped the optimistic bubble, so the prompt vanished entirely and had
to be retyped.

sendPrompt now resolves false when the runtime never took the prompt and
retracts the optimistic user bubble; the thread pane restores the text and
attachments to the composer (unless the user has typed something since).

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

* desktop: merge restored attachments with ones added during the failed send

Reuse handleAttachFiles (which already dedupes by name/size/mtime) instead
of an either-or restore, so attachments added while the send was pending no
longer drop the failed submission's attachments.

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

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-09 18:04:35 -07:00
Saoud RizwanandSaoud Rizwan 038102987e fix(desktop): stop the sidecar-hosted Hub daemon before the Windows installer writes code-sidecar.exe (#14006)
* fix(desktop): stop the sidecar-hosted Hub daemon before the Windows installer writes code-sidecar.exe

The compiled sidecar re-executes itself as the detached Cline Hub daemon,
which outlives the app by design. Tauri's NSIS installer only kills the main
binary, so on update the daemon still held code-sidecar.exe and the install
failed with "Error opening file for writing" until the user killed the
process by hand.

Add an installerHooks .nsh that terminates code-sidecar.exe in
NSIS_HOOK_PREINSTALL and NSIS_HOOK_PREUNINSTALL, mirroring what Tauri does
for the main exe. Shipping this in the installer also fixes updates into the
next release from any older version.

Closes CLINE-3222 / #13992

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

* fix(desktop): scope the installer's sidecar kill to this install's code-sidecar.exe

The production Hub is shared per machine, so a code-sidecar.exe from another
install (the side-by-side Cline Beta) may be hosting it without locking ours.
Match on the full path instead of the image name so updating one channel
does not take down the other's sessions. The path is passed through an
environment variable so $INSTDIR never needs quoting in the PowerShell
command.

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

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-10 09:59:37 +09:00
Saoud RizwanandSaoud Rizwan 00fa85c164 fix(desktop): show Codex-only model list for ChatGPT Subscription provider (#14010)
The model-list path passed a provider config with the default
includeKnownModels, which fills knownModels with the raw openai-native
catalog. mergeKnownModels spreads that in after the Codex filter, so the
picker showed GPT-4.1 / GPT-4o / chatgpt-image-latest etc. Pass
includeKnownModels: false like the CLI already does.

Fixes CLINE-3232

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-09 17:48:58 -07:00
Saoud RizwanandSaoud Rizwan 08df88d6ec fix(desktop): let local-auth and catalog OAuth providers start sessions without an API key (#14007)
resolveCredentialError only exempted a hardcoded OAuth id set, so Claude Code
and Codex CLI showed as Configured in Settings (via the local-auth capability)
but session start still refused them with "Missing API key". Gate on the
provider catalog's oauth/local-auth capabilities as well.

Fixes CLINE-3238, ENG-2466

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-09 17:31:22 -07:00
Bee 01aeb2147d fix(desktop): distinguish model names and separate ClinePass tiers CLINE-3230 CLINE-3231 (#13996)
* fix(desktop): distinguish duplicate model names in the picker

* fix(desktop): separate ClinePass subscription and free fallback models
2026-09-09 17:13:19 -07:00
Bee 1063db45d8 chore(sdk): model list version update (#14001)
Update to 1788986887659 from bun run build:models
2026-09-09 14:22:49 -07:00
Saoud RizwanandSaoud Rizwan 194214af76 ci(desktop): stop Tauri from skipping the DMG layout AppleScript on Actions (#13998)
Tauri's DMG bundler passes --skip-jenkins to bundle_dmg.sh whenever CI=true,
which GitHub Actions always sets. That skips the Finder AppleScript that
applies the background, window size, and icon positions from tauri.conf.json,
so every published DMG since #13563 shipped with the stock Finder window even
though the artwork was generated and validated. Set TAURI_BUNDLER_DMG_IGNORE_CI
so the script runs on the macOS runner's GUI session.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-09 12:18:30 -07:00
Saoud Rizwan 2dd8f11ada Reformat CLI section for improved clarity
Split a long line into two for better readability.
2026-09-09 01:02:22 -07:00
Saoud RizwanandClaude Opus 5 bf4a364e08 fix(desktop): pin the macOS app-icon closure's error type
set_app_icon's main-thread closure ends in Ok(()) and uses ? on String
errors, so its error type was only constrained by E: From<String> —
ambiguous, since String has many From impls (E0282 + E0283). The block is
behind #[cfg(target_os = "macos")], so the Windows build compiled past it
and desktop-publish.yml is the only workflow that builds the Tauri macOS
binary, which is why this reached main and only surfaced when cutting
v0.0.24.

Annotate the closure as Result<(), String>.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBdgHNDCfm7WKP8qbvn7cr
desktop-v0.0.24
2026-09-09 00:56:59 -07:00
Saoud Rizwan fee4fb96f2 Improve README CLI section formatting
Removed line break for better readability.
2026-09-09 00:38:16 -07:00
Saoud RizwanandClaude Opus 5 eb18ce3407 chore(desktop): note the queued-prompt and observer-gate fixes in v0.0.24
Adds #13979 (queued prompt's user bubble) and #13981 (new-task idle
flicker), and rewrites the stream-duplication entry: #13978 replaced the
timer-based observer standdown from #13968/#13976 with a direct
ClineCore subscription check, so the shipped mechanism is no longer the
5s/busy-run window the note described.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBdgHNDCfm7WKP8qbvn7cr
2026-09-09 00:31:47 -07:00
Saoud RizwanandClaude Fable 5.1 9fb1e6fc25 fix(desktop): keep a new task on "starting" while the hub reports the fresh session idle (#13981)
* fix(desktop): keep a new task on "starting" while the hub reports the fresh session idle

Sending the first prompt of a new task sets the session to "starting" and
issues the start RPC. While that RPC is in flight the hub publishes
session.created and two session.updated events carrying the new record's
status, "idle", and the sidecar forwards each as chat_session_status. The
webview applied them over "starting", then flipped back to "running" once
run.started arrived, so on every new task the composer placeholder and the
request indicator switched to the idle state and back for a frame.

The status handler already drops a "running" that trails a settled turn
as stale. Add the reverse guard: while a local prompt submission is in
flight, a non-busy status predates the run it is about to start and is
dropped. The submission owns status until it hands off, to the queued
start event or to its own completion for a blocking send. Once nothing is
in flight the hub's status applies as before, so the idle a drained turn
relies on is unaffected.

Test replays the sequence with the sidecar reusing the planned session id
(as it does): idle during the start RPC leaves "starting", the queued send
then reaches "running", and an idle afterwards applies. Disabling the
guard fails it with "expected starting, received idle".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wnz9AtY5MrXddi9sfdzJnr

* fix(desktop): hold back only the transient idle while a submission is in flight

Narrow the new guard from every non-busy status to "idle". The
created-session flicker is always an idle, and a terminal status (failed,
aborted) that lands during a submission is real: it must still unstick the
UI if the send response never arrives. Test pins that a failed status
during the start RPC is applied.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wnz9AtY5MrXddi9sfdzJnr

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 00:29:48 -07:00
Saoud RizwanandClaude Fable 5.1 40f1d75bd2 fix(desktop): keep a queued prompt's bubble when the previous send resolves after it starts (#13979)
* fix(desktop): keep a queued prompt's bubble when the previous send resolves after it starts

When a prompt is queued behind a blocking send, the runtime drains the queue
before it answers that send: `LocalRuntimeHost.runTurn` schedules the drain
as a microtask right before returning the result. So the queued prompt's
`chat_queued_prompt_start` reaches the webview ahead of the previous turn's
send response, the webview appends the new user bubble, and then the
blocking send's completion path runs.

That path treated the transcript as its own: it re-applied the result's
assistant text (minting a fresh bubble, since the queued start had already
reset the active assistant id), materialized tool rows, and replaced the
whole transcript from a canonical read. The transcript is persisted only at
iteration boundaries, so at that moment the canonical read ends at the
previous turn's assistant message and the replace erased the queued user
bubble. The reply then streamed in under no user message; the bubble only
came back when the queued turn's own reconcile ran, or on re-hydrate.

Make the blocking-send completion path defer to a newer turn the same way
the queued branch already does: once the epoch has moved past the one
captured at dispatch, skip every transcript write (assistant text, media,
tool rows, canonical recovery and replace) and leave the live refs alone in
the `finally`, since they belong to the turn in flight. The newer turn's
completion reconciles history when it ends. Token and cost bookkeeping
still applies.

Regression test replays the exact order: blocking send in flight, its
text streamed, queued-start for the next prompt, then the send resolves
with a canonical read that predates the queued message. Disabling the guard
fails it (the user bubble is gone and the essay appears twice).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wnz9AtY5MrXddi9sfdzJnr

* fix(desktop): settle a queued turn from the session status when chat_done never arrives

A turn drained from the prompt queue does not always deliver `chat_done`.
The hub runtime host suppresses a second `done` for a session until it
sees `run.started`, and a drained turn never publishes one; when the
queue command lands while the previous turn is still running (the normal
way to queue), the previous turn's `done` re-arms that suppression and
the drained turn's `done` is swallowed. The sidecar's chunk log for every
such turn today ends at `chat_usage`.

Without `chat_done` nothing settles the turn in the webview: the assistant
bubble stays in its streaming state and the persisted-history reconcile
never runs, so live rows keep whatever the stream produced. This is why a
queued prompt's reasoning row stayed on "Thinking" after its reply, and
why the earlier wiped user bubble never came back on its own.

The hub's session status is authoritative and already reaches the webview
as `chat_session_status`. When it reports a non-busy status and the stream
has not settled the current turn, settle it there: clear the streaming
state and schedule the same reconcile `chat_done` would have. A turn that
`chat_done` already settled is left alone (epoch equality), as is an
aborted one. The idle the hub publishes between a finished turn and the
drained one it hands off to also lands here; the queued prompt's start
bumps the epoch before that reconcile fires, so it is skipped.

Tests: one replays queued-start → reasoning → text → status idle with no
`chat_done` and asserts the streaming id clears and the transcript is
reconciled; one asserts a trailing idle after `chat_done` schedules no
second reconcile. Disabling the fallback fails the first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wnz9AtY5MrXddi9sfdzJnr

* Revert "fix(desktop): settle a queued turn from the session status when chat_done never arrives"

This reverts commit 8ee3f40edf.

* fix(desktop): stamp live chat rows on the webview clock, not the sidecar's

The thought duration on a reasoning row is the row's timestamp minus the
preceding row's. For a typed prompt the preceding row is the optimistic
user bubble, stamped with the webview's clock, while every live row the
stream produced was stamped with the sidecar's `ts`. Those are the same
clock in the packaged app but not when the sidecar runs elsewhere (the
browser-dev container, a remote hub). With the sidecar's clock trailing the
browser by more than the time to first token, the subtraction went
negative, the duration was dropped, and a finished reasoning row rendered
as a durationless "Thinking" under its brain icon instead of "Thought for
Ns". The previous turn's canonical replace used to hide it by re-rendering
from runtime-stamped rows; now that a newer turn correctly keeps its live
transcript, it showed.

Every timestamp a live row is compared against is this process's clock:
the optimistic bubble, `hydrationStartedAt`, `turnStartedAt`, and the
preceding row in the duration subtraction. Two of those comparisons were
already mixing clocks. Stamp live rows with `Date.now()` on arrival so all
of them are consistent; persisted rows keep the runtime's timestamps and
stay consistent among themselves.

Tests: a new one streams reasoning with a sidecar `ts` 15s behind the
browser and asserts the row still yields a thought duration; the old
"keeps live stream timestamps in milliseconds" test pinned the sidecar
timestamp and is rewritten to assert the webview clock.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wnz9AtY5MrXddi9sfdzJnr

* fix(desktop): leave status to the queued turn when the previous send resolves late

After a queued prompt has started its turn, the previous blocking send's
completion path still ended by settling status: `setStatus("completed")`
and `turnSettledEpochRef = turnEpochRef`, i.e. it settled the new turn's
epoch, not its own. Two visible effects. The composer left the busy state
while the queued reply was still pending, so nothing indicated a request
was in flight after the queued message went out. And the hub's
`session.updated running` for the new turn, which arrives afterwards, was
dropped by the stale-"running" guard, since a "running" at a settled epoch
reads as stale.

When a newer turn owns the transcript, leave status and the settled epoch
to it: its start set "running", and its own completion settles it. The
failure text for an errored previous turn is still appended.

The queued-bubble test now asserts the session stays "running" after the
late response and that a following "running" status is applied; forcing
the guard off fails it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wnz9AtY5MrXddi9sfdzJnr

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 00:12:10 -07:00
Saoud RizwanandSaoud Rizwan b18de0904f fix(desktop): gate the observer stream on ClineCore's subscription, not a timer (#13978)
* feat(core): expose whether ClineCore is subscribed to a hub session

HubRuntimeHost subscribes to a session as a side effect of starting,
sending to, or listing pending prompts for it, and unsubscribes on stop.
Clients that also observe the hub directly need that fact to decide
which copy of a session's events to render.

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

* fix(desktop): gate the observer stream on ClineCore's subscription, not a timer

The sidecar has two hub sockets that both receive a session's events:
ClineCore's own client and the observer client. #13968 and #13976 muted
the observer's copy by inferring whether ClineCore was serving the
session from a per-session timestamp (refreshed on every core event,
expired after 5 s, held while busy, and manually forgotten on every
stop path). That inference had to be patched twice and still raced the
first event of each run.

Ask ClineCore instead. hasSessionSubscription is the fact the timestamp
was approximating, it is set before the subscribe frame is even sent
and cleared by the same stop that disposes the subscription, so there
is nothing to refresh, expire, or forget. The observer projection is
skipped as a whole (status and ended too, which the core pipe also
carries), and the boot-id fix from #13968 is unchanged.

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

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-08 21:24:58 -07:00
Saoud RizwanandClaude Opus 5 de1d2a322c chore(desktop): release v0.0.24
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBdgHNDCfm7WKP8qbvn7cr
2026-09-08 20:03:56 -07:00
Bee 245a7d0ccb fix(desktop): show token usage and cost for every visible session-history page (#13971)
* fix(desktop): hydrate session usage for the visible page, not just the first four rows

Discovery rows carry no token or cost totals; the history hook sums them
from each session's transcript in a second read, and that read was
hard-coded to the four most recent sessions. Every other row in the
sessions view rendered "-" for tokens and cost, and paging never asked
for more.

Widen the default window to the first ten rows (one page of the sessions
view, and the sidebar's initial threads), expose requestUsage() so a view
can ask for the rows it is showing, and have the sessions view report its
visible page so moving to older pages fills them in on demand. Transcript
reads are capped at four in flight, since each one parses a whole session
file in the sidecar.

* fix(desktop): enforce the usage-read cap across hydration effect restarts

The four-read cap was a counter local to one run of the hydration effect.
A session refresh or page change restarts the effect while reads are
still pending, and the new run started from zero, so four more reads
could join the four already in flight.

Check the cap against usageLoadingRef, which counts every read in flight
across runs, and have a finishing read pump the current run's queue via
usagePumpRef so a freed slot goes to the newest queue. Track hydrated
usage in a synchronous usageByIdRef instead of threadsRef, which lags
React's commit and made a just-finished row look unhydrated and get read
again. The delete handler no longer clears the in-flight gauge for a row
whose read is still running.

The regression test restarts the effect with four reads pending and
checks that no fifth read starts, that the restarted queue still drains
as the earlier reads finish, and that no session is read twice.

* fix(desktop): re-read a session whose status changed while its usage read was pending

A restarted hydration run dropped any row that already had a read in
flight. If the row's status had changed in the meantime (a running
session finishing is the common case), the pending read's result was
already stale when it landed, and nothing read the row again until some
later refresh happened to restart the effect. The row could sit on the
totals from before its last turn indefinitely.

Record the status each in-flight read was started under in
usageLoadingRef. A restarted run skips a row whose pending read was
started under its current status and defers one whose status has moved:
the row stays queued, and when the pending read settles and records the
status it was started under, the mismatch makes the next pump read the
row again ahead of rows never read.

Also bound the on-demand set: requestUsage now replaces the requested
ids instead of accumulating them, and the sessions view releases its page
on unmount, so a running session on a page the user has left is not
re-read on every refresh. The equality guard keeps the same Set instance
when the members are unchanged, so the view re-reporting its page on
every threads change does not restart the effect.

Tests: the status-change case (four reads pending, session-3 goes
running -> completed, its stale read finishes, it is read once more
before session-4) and the release case (a running row is re-read while
requested and left alone after the request is cleared). Both fail on the
previous commit.
2026-09-08 19:52:59 -07:00
Saoud RizwanandClaude Fable 5.1 23f2197c53 fix(desktop): ask the user how to continue when the mistake limit trips instead of stopping silently (#13969)
* desktop: ask the user how to continue when the mistake limit trips

The core's loop detector stops a run after 5 identical consecutive tool
calls (and the mistake tracker after 6 consecutive failures) by asking the
client for a decision via onConsecutiveMistakeLimitReached. The desktop
never registered that callback, so the SDK default "stop" applied and the
webview rendered the result exactly like the Stop button: the composer went
idle with no message. Users on models that fall into identical-call loops
(reported with cline-pass/kimi-k3 re-sending `editor` with old_text: null)
saw Cline "randomly stop" mid-task, and a nudge died after one more call
because the identical-call counter survives across turns.

Mirror the CLI's interactive handling (apps/cli/src/runtime/interactive/
mistakes.ts): route the decision through the sidecar's existing ask-question
channel with "Try a different approach" / "Stop this run". The prompt reads
the session id lazily because fresh starts only learn it after
manager.start() resolves and the webview matches prompts by active session.

On continue, steer the guidance into the running turn via manager.send
delivery "steer". The core appends its own guidance to a transcript store
the live runtime never reads mid-run, so without this the model would
resume with no idea why it was paused and repeat the same call.

Wired into every desktop start path: start, provider-change rebuild, fork,
and checkpoint restore. No webview changes; it already renders ask-question
requests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJb3VtvdSm9xk9opJ3Q9G6

* fix(desktop): make mistake-limit recovery pause and cancel reliably

* fix(desktop): keep mistake recovery within the desktop app

* fix(desktop): wait for mistake recovery decisions before continuing

* fix(desktop): settle unfinished tool rows when a run stops

* fix(desktop): harden mistake recovery and terminal cleanup

* fix(desktop): confirm session status before settling tools

* fix(desktop): scope stopped tool recovery to mistake prompts

* fix(desktop): deliver mistake guidance only through steering

* fix(desktop): simplify stopped tools to a rendering change

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 19:45:24 -07:00
Saoud RizwanandClaude Fable 5.1 9df2876c89 fix(desktop): keep the observer stream stood down for the whole busy run (#13976)
* fix(desktop): keep the observer stream stood down for the whole busy run

The core-pipe liveness mark from #13968 expired after 5s of silence even
mid-turn. Long commands, slow first tokens, and unanswered tool approvals
stall both hub pipes together, so the observer's copy of the first event
after such a gap arrived ahead of the core copy and was emitted, doubling
a delta or leaving a duplicate tool row stuck at "start" until the
turn-end reconcile.

While the session is busy, treat the mark as active regardless of age;
the 5s window now only governs idle sessions. The mark is still cleared
on session end, so an observer-only session is unaffected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kHp3XEsLYaZ1Bgp4yUxf3

* fix(desktop): forget the core pipe mark when the sidecar stops a session

stop() disposes the ClineCore subscription without any local `ended`
event, so the activity mark from the previous run survived. With the
mark now treated as active for the whole busy run, a later run another
client started on the same session would set busy via the observer's
run.started and every observer chunk would be dropped with no core
subscription left to serve it.

Clear the mark on every sidecar stop path (stop command, provider-change
rebuild and its rollback, reset). Abort keeps the subscription and needs
nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kHp3XEsLYaZ1Bgp4yUxf3

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 19:26:43 -07:00
John Choi b63c738c22 feat(sdk): support authenticated remote Hub connections (#13519)
* feat(sdk): support authenticated remote Hub connections

* fix(sdk): pin compatible SAP connectivity

* chore(sdk): defer SAP smoke fix to main

* fix(sdk): preserve hub connection failures

* docs(sdk): clarify remote Hub connection headers

* test(sdk): tighten hub header coverage

* test(core): batch root history fixture inserts

* test(sdk): await daemon health after discovery publication
2026-09-08 16:59:57 -07:00
Saoud Rizwan bbacedd437 fix(desktop): preserve Cline Pass model selection across new chats (#13975) 2026-09-08 16:57:58 -07:00
Dominic CooneyandSaoud Rizwan 4096eca8a1 feat(desktop): custom Windows title bar (#13831)
* feat(desktop): add custom Windows title bar

* fix(desktop): keep Windows caption controls inside the compact title-bar row

The fixed caption controls stayed h-12 when the title-bar row shrinks to
its max-md:h-7 compact height, so they overlapped page content in narrow
windows. The controls now follow the same responsive height.

Also cover the resize-driven Maximize/Restore label transitions with a
test that invokes the captured onResized listener.

* fix(desktop): keep Windows caption controls above overlays

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-09-08 16:45:26 -07:00
Saoud RizwanandClaude Fable 5.1 2f0f78bb8e fix(tools): tell the model how to recover when editor old_text is null (#13970)
* fix(tools): tell the model how to recover when editor old_text is null

The editor schema declares old_text as nullable+optional, so the JSON
schema the model receives is anyOf[string, null]. Models that fill optional
parameters with null (observed with cline-pass/kimi-k3) then hit a terse
"old_text is required" error for existing files and re-send the identical
call until the loop detector stops the run.

Spell out in the schema description that null/omitted is only valid when
creating a file or inserting via insert_line, and make the executor error
name the file, say whether old_text was null or omitted, and state the
recovery so the next call has a reason to differ.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJb3VtvdSm9xk9opJ3Q9G6

* revert schema description and test changes; keep only the executor error message

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJb3VtvdSm9xk9opJ3Q9G6

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 16:14:57 -07:00
Saoud RizwanandClaude Opus 5 62ba65397f fix(desktop): stop the live chat stream from doubling and dropping chunks (#13968)
* fix(desktop): emit each streamed chat chunk once

The sidecar has two pipes into `emitChunk`: the ClineCore session
subscription (`handleCoreSessionEvent`) and the hub observer client
(`handleHubLiveEvent`, gated on `attachedViaHub`). Opening a session arms
both — the hydrate's `pending_prompts` call makes `HubRuntimeHost`
subscribe to the session, and `attach` sets `attachedViaHub` — so for a
session streaming through the hub every delta was emitted twice.

Neither existing guard caught it. Both copies go through `emitChunk`, so
each gets its own increasing `index`, which is what the webview's replay
guard compares; and the webview's `endsWith` fallback is defeated by the
50ms coalescing buffer, which concatenates the duplicated deltas before
comparing them. `attachedViaHub` is cleared when the webview *sends*, so
this only showed on sessions that stream without a local send first — a
run already in flight when the task is opened, a resumed or scheduled run
— and the canonical store was always clean, so reopening the task
rendered correct text.

Arbitrate instead of guessing which pipe owns a session: the first source
to deliver a contended stream wins and the other is muted until the owner
falls silent for 5s. Ownership is per stream, so a pipe that wins one
stream cannot mute another it does not itself carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNoho8fGgnh71xjQF5xMqy

* fix(desktop): keep rendering the live stream when the sidecar restarts

`shouldApplyStreamChunk` drops any chunk whose `index` is not above the
highest one already seen for that session. That counter lives in the
sidecar process (`ctx.streamIndices`), but the webview only cleared its
high-water mark in `hydrateSession`, never on reconnect. So when a sidecar
was replaced under a live webview — crash-respawn, hub drain-and-replace,
a stale-sidecar swap — the replacement started numbering at 1 again and
the webview silently discarded everything for that session until the new
process counted past the old run.

The guard runs ahead of any per-stream handling, so this dropped far more
than assistant text: `chat_queued_prompt_start` (the user's own message
bubbles) and the tool-call rows went with it. The transcript only looked
broken live — the turn-end reconcile and switching tasks both re-read
canonical history and repaired it, which is why it presented as rows that
vanish mid-turn and come back afterwards.

Stamp each chunk with the emitting sidecar's boot id so a counter reset is
a fact rather than an inference: a changed boot id means a new process, so
the mark is rebased instead of swallowing the stream. Replays from the
same process are still dropped exactly as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNoho8fGgnh71xjQF5xMqy

* refactor(desktop): let the core pipe decide, not a list of stream names

The first pass arbitrated a hardcoded set of six `chat_*` stream names —
a third copy of knowledge already encoded in the two switch statements
that emit them, and one a later stream would silently drift out of.

The two pipes are not peers, so they do not need symmetric arbitration.
ClineCore's session subscription is the primary; the hub observer's
projection exists to cover sessions ClineCore is not subscribed to. Any
event reaching `handleCoreSessionEvent` proves it is subscribed, so that
pipe records its own liveness and the observer stands down while it is
serving. Dropping an observer chunk for a stream the observer never
produces is a no-op, so the list has nothing left to do.

Liveness is marked from the pipe rather than from `emitChunk`, so chunks
the sidecar synthesizes locally never claim to be the core subscription.

Net: the stream-name list, the per-stream owner map and its type are
gone, and detection now starts at the session's first core event of any
kind instead of its first contended chunk.

Also fills in `coreStreamActivity` on the partial `as unknown as
SidecarContext` fixtures in chat-session.test.ts — they bypass the type
checker, so a missing field only surfaces as a runtime crash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNoho8fGgnh71xjQF5xMqy

* docs(desktop): record why core-pipe liveness marks on every event

Review asked why status and queue events mark the pipe active when they
carry no chat content. Marking only on content would be worse, and the
reason is not local to this function: the hub fans out to listeners in
registration order, so the observer's global subscription (registered at
sidecar boot) sees each delta before this per-session one (registered at
hydrate). Waiting for core content to establish the mark would let the
observer's copy of a turn's first delta through before the mark existed,
doubling it every turn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNoho8fGgnh71xjQF5xMqy

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 16:04:14 -07:00
Saoud RizwanandSaoud Rizwan 79bf1e8c48 Imported sessions: warn in the chat and summarize the foreign history on resume (#13964)
* desktop: flag sessions imported from other coding agents in the chat

Imported Claude Code / Codex / opencode transcripts keep the source tool's
own tool names and schemas, so resuming them in Cline can behave worse than
a native session. Lead the transcript with a notice naming the source tool
so the user knows why.

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

* core: summarize imported sessions on their first resumed turn

Imported transcripts keep the source agent's tool names and schemas, which a
model continuing them may try to call. When a session marked importedFrom is
resumed without a compaction sidecar, run a manual agentic compaction over
the whole foreign history before the first model request. The summary lands
in the sidecar, so it runs once and the canonical transcript stays intact;
on failure the turn falls back to the raw history.

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

* desktop: show a status row while an imported session's history is summarized

Core tags the resume-time compaction notices with the source tool; the sidecar
now forwards notice metadata and the webview turns the started/completed pair
into one in-place status row in the transcript.

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

* desktop: keep the imported-history summary row through canonical rehydration

The row is client-only, so applyCanonicalHistory re-seats it by timestamp
instead of dropping it when the persisted transcript replaces live state.

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

* desktop: name the imported-history summary in the pre-output indicator instead of a transcript row

The summary is not part of the persisted transcript, so a client-only row
had to be re-seated after every canonical rehydration. Show it where the
ephemeral state already lives: the "Thinking..." indicator reads
"Summarizing the imported <tool> history..." while it runs, and the
imported-session notice states that the model works from a summary.

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

* review cleanup: extract imported-history compaction policy, fold client helper into session-import

- core: createImportedHistoryCompactionPrepareTurn lives in compaction.ts
  next to the other prepareTurn builders; the host only decides when it
  applies. Fix a tool_result fixture missing its name (tsc, not vitest).
- desktop: readImportedHistorySummaryActivity moves into session-import.ts
  alongside readImportedFromTool, with one test file for both.
- trim comments; note the policy in sdk/ARCHITECTURE.md.

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

* docs: note the imported-session compaction policy in sdk/ARCHITECTURE.md

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

* address review: retry imported-history summary after abort, stand down on a projected sidecar, clear the activity label on hydrate

- An aborted summarizer call no longer consumes the single attempt, so the
  next turn retries instead of replaying the raw foreign transcript.
- The policy now applies to every imported resume and skips only when the
  working context already opens with a compaction summary. A stale sidecar
  that fails projection therefore gets re-summarized rather than bypassed,
  and the host no longer gates on the sidecar's mere existence.
- hydrateSession resets activityLabel like the rest of its per-turn state.

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

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-08 15:21:57 -07:00
Saoud Rizwan ffd65af086 fix(core): keep root sessions in history when child rows crowd the scan window (#13887)
* fix(core): keep root sessions in history when child rows crowd the scan window

listSessionHistory over-fetched a fixed 2x window from the backend and then
dropped subagent/team-task child rows. Children always sort after the root
that spawned them, so one session with more children than the window hid
itself and every older root, and the desktop sidebar (limit 50) rendered an
empty history with no way to load more. Widen the scan until the requested
page of roots fills or the backend runs out of rows.

* fix(core): filter root sessions in persistence for history listing

Review follow-up: widening the client-side scan still hit the 2000-row cap,
so 2000+ child rows ahead of a root left history empty. Add a rootOnly option
to the persistence adapters (SQL WHERE on is_subagent / parent_session_id),
carry it through UnifiedSessionPersistenceService, RuntimeHost, both host
implementations, and the hub session.list payload, and have history listing
request it. The client-side filter and widening stay as a fallback for older
hubs that ignore the flag.

* test(core): pin rootOnly forwarding in LocalRuntimeHost and document root-only history listing
2026-09-08 14:32:00 -07:00
Saoud RizwanandSaoud Rizwan 119fa4ea03 feat(core): mark imported sessions with an import history origin and stamp it on telemetry (#13886)
* feat(core): stamp imported sessions with an import history origin

Imported sessions now carry sessionHistoryOrigin { mode: "import", trigger: <tool> }
alongside the existing importedFrom marker, so the messages file origin block and
downstream telemetry can separate transcripts that did not originate in Cline. The
top-level source stays the client surface (desktop), matching how scheduled runs
record automation/hub-schedule.

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

* fix(core): keep the stored history origin when resuming a session

The start input always carries a default user origin, which the resume path merged
over the manifest's metadata and then persisted on the first git metadata refresh,
so automation and import provenance was lost as soon as a session was continued.

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

* feat(desktop): show the source agent icon on imported sessions

Sidebar rows and the Sessions list render a Claude / OpenAI / opencode mark (Simple
Icons, CC0) next to sessions imported from that agent, and the hover card lists the
import source.

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

* Revert "feat(desktop): show the source agent icon on imported sessions"

This reverts commit c9dd674487.

* refactor(core): inline the import history origin mode

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

* fix(core): drop the stored trigger when a resume overrides the mode

An explicit start-input mode now replaces the stored history origin as a
whole instead of pairing the new mode with the previous trigger.

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

* feat(core): stamp session_origin on every telemetry event a session emits

The runtime host resolves the session's history origin before bootstrap and
the bootstrap scopes the session telemetry with session_origin (mode) and
session_origin_trigger, using the same non-owning scope the Hub already
applies for client identity. Errors from imported transcripts can now be
filtered with session_origin = import.

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

* test(core): use CORE_TELEMETRY_EVENTS constants in session origin tests

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

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-08 14:31:22 -07:00
Saoud RizwanandSaoud Rizwan 40a7e6526d fix(core): do not build a file index for the home directory or filesystem root (#13960)
* fix(core): do not build a file index for the home directory or filesystem root

Running cline from $HOME and typing an @ mention could take the TUI to
many GB of RSS and get it OOM-killed: the file index listed every file
under the home directory and the mention picker re-ranks the whole index
on each keystroke. Skip indexing entirely when the workspace root is the
home directory or filesystem root.

Refs #13930, #13905

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

* fix(core): canonicalize paths in the home/root index guard

Compare realpaths so a symlinked or differently-cased (Windows) spelling
of the home directory still hits the guard. Add a filesystem-root test.

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

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-08 13:55:43 -07:00
Bee 3d5070705c feat(desktop): Enable web search by default outside YOLO mode CLINE-3214 (#13957)
* feat(core): enable web search by default outside yolo

* fix(core): fail closed when tool settings cannot load
2026-09-08 22:11:14 +02:00
Mikołaj Kondratek 5a3b870d85 fix(core): stop checkpoints from re-hashing unchanged untracked files every turn (#13199)
* fix(core): stop checkpoints from re-hashing unchanged untracked files every turn

Checkpoint creation rebuilt a throwaway GIT_INDEX_FILE on every user
turn, so git re-read and re-hashed every untracked file before each
model call — with multi-GB untracked data files this blocks every
message for seconds to minutes (~90s in the report from #13131, on a
cloud-synced Windows workspace).

Keep one snapshot index per session in a scratch dir instead: git's
stat cache then skips unchanged files, and from the second turn the
snapshot cost is roughly git process overhead. Entries that fall out
of the untracked set (file deleted or became tracked) are removed each
turn so they cannot ghost into snapshot trees; a corrupt index heals
with one rebuild-and-retry; deleteCheckpointRefs removes the scratch
dir with the refs.

Also adds two telemetry events so checkpoint cost is observable in the
field: checkpoint.snapshot (outcome + duration per snapshot attempt)
and checkpoint.restore (outcome + duration per restore). Durations and
outcomes only — never file paths.

Snapshot contents are byte-identical to before — no size caps, no
timeouts, no restore-behavior changes. Those are tracked separately
pending the team decision on checkpoint semantics.

Part of #13131

* fix(core): harden the persistent checkpoint scratch index

Self-review findings on the persistent-index change, applied together
because they share one root cause: a throwaway mkdtemp directory became
a durable, addressable one, which changed the failure model.

- Relocate the scratch dir from the world-shared OS tmpdir to
  <cline-data-dir>/checkpoint-scratch/<sha256(cwd+sessionId)>, created
  0700. The index and pathspec files enumerate workspace paths, so they
  no longer live world-readable at a guessable path; hashing removes
  sanitization collisions between distinct session ids and keys the
  cache to the workspace it was built from.
- Clear index.lock alongside index in the rebuild path: a git process
  killed mid-add leaves the lock behind, and without this every later
  turn of the session failed the add and degraded to HEAD-only
  checkpoints permanently.
- Do not rebuild on pathspec-match failures (a listed file vanishing
  before the add): that is a per-turn race, and rebuilding threw away
  the whole cross-turn stat cache for it.
- Normalize trailing slashes in the stale-entry sweep: ls-files reports
  an untracked nested repo as "sub/" while the index records the
  gitlink as "sub", so the sweep purged the gitlink the same turn it
  was added and snapshots silently lost nested repos.
- Replace the argv-batched update-index loop with one "-z --stdin"
  invocation (the no-pathspec-from-file workaround was built on a false
  premise).
- Reap scratch dirs idle for 14 days when hooks are created; explicit
  session deletion still removes them immediately. Previously only
  deleted sessions ever cleaned up, leaking one index per session.
- Emit checkpoint.restore from the hub restore handler too (the path
  most hosts use), and capture restore failures that happen during
  validation and planning — previously the most common failures
  produced no event and durations excluded the message read.

Regression tests: stale-lock recovery, nested-repo gitlink retention
across turns, and a failed-validation restore event.

* fix(core): pin scratch-index git config so change detection survives core.ignorestat

A persistent GIT_INDEX_FILE inherits whatever the repo's config makes git
write into it. With core.ignorestat=true, git marks every entry it adds
as assume-unchanged and stops stat-checking it, so a file changed after
the first turn kept its stale content in every later snapshot. The
original per-turn throwaway index never carried that bit across turns.

Pin core.ignorestat=false (and core.splitIndex=false, which would scatter
shared-index files for our private index into the user's .git) on every
command that touches the scratch index. Regression test reproduces the
before/after! case from review.
2026-09-08 20:41:46 +02:00