getAuthToken captured expiresAt before refreshing, then validated the new
token against that stale value. When the old token was already past expiry
(not just inside the 5-minute buffer), a successful refresh was thrown
away and null returned, so the first call after long idle failed despite
valid credentials. Re-read the expiry from the refreshed auth info.
* fix(hub): don't forward recoverable agent errors to dashboard peers
Recoverable error events are in-run notices, not turn outcomes: the
MistakeTracker emits one for every recorded mistake (e.g. a plan-mode
guard-blocked run_commands call) while the run continues. The hub
dashboard forwarded every error event to peers, so the webview dropped
out of the sending state and appended an error row mid-turn — the same
host bug fixed for VS Code and the CLI in #12953.
Gate the forward on recoverable, matching those hosts: the tool failure
is already shown inline via the failed tool_event, and the turn's
outcome stays decided by how it actually ends (turn_done or a
non-recoverable error). Recoverable errors are logged server-side.
* fix(hub): forward recoverable flag to peers instead of filtering server-side
Per review: the server is a translation layer between agent events and
the webview protocol, so it should not embed display policy or console
logging. Forward every agent error with its recoverable flag on the
peer message and let each peer decide — the webview keeps recoverable
errors out of the transcript and keeps the turn state, matching how the
CLI gates display on the same flag while the information stays
available to any peer that wants it.
* add custom model selection to the vertex provider
* fix race conditions from PR review
* fix linter warnings
* fix test failures
* refactor(vscode): drop Vertex global-endpoint picker filtering
The SDK catalog is live (models.dev), so a static host allowlist of
global-endpoint-capable models lags every model launch and silently hides
new models from users on vertexRegion=global. Remove the allowlist, the
host override that injected supportsGlobalEndpoint, and the picker filter;
show the full catalog for every region.
An unsupported pick now fails loudly at request time: map Vertex's
'model not available in region: global' (and Google's Publisher Model
locations/global not-found body) to recovery guidance in the error row.
Also drop Anthropic's universal pricing from the Vertex Fable 5 overlay —
Vertex bills region-dependently, so the copied price understated recorded
cost; the record now carries no pricing instead of a wrong one.
---------
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
* fix: remove stale Double-Check Completion feature tip
The rotating feature tips still told users to enable "Double-Check
Completion" in settings, but that toggle was removed in the new UI —
the Features section now offers Auto Compact, Feature Tips, Background
Edit, Checkpoints, Worktrees and Hooks. Following the tip sent users
searching the settings panel for something that isn't there.
Drop the tip. The remaining ten were checked against the current UI and
all still hold, including the "Settings → Features → Feature Tips" path.
* chore: remove dead CLI settings e2e page object and orphaned test
`page-objects/settings.ts` asserted the CLI settings Features tab shows
"Double-check completion" — the same removed setting behind the stale
feature tip. Nothing in the live tui-test suite (apps/cli/src/tests)
imported it; only chat.ts and auth.ts page objects are in use.
Its one importer, apps/vscode/tests/e2e/cli/interactive.test.ts, is a
leftover from the pre-2026-06-02 SDK migration squash: all three of its
imports resolve to files that don't exist, there's no tui-test config in
that tree, and no npm script runs it. It cannot execute.
* fix: respect user max output tokens in compaction summarizer requests
The compaction summarizer hardcoded max_tokens to 1024 and the VSCode host
never mirrored the user's Max Output Tokens onto providerConfig, so summary
requests were always capped at 1024 tokens. Reasoning models can spend that
entire budget thinking; the reasoning stream is discarded, so no summary
text arrives and compaction is skipped on every attempt.
- Mirror maxTokensPerTurn onto providerConfig.maxOutputTokens in the VSCode
session factory so consumers that build handlers straight from it (the
compaction summarizer) honor the user's setting, matching the CLI.
- Resolve the summarizer output budget from explicit config, then model
info, then knownModels, before the default; raise the default to 4096.
- Log a diagnostic warning (reasoning chars, incompleteReason, likely
cause) when the summarizer returns no summary text instead of silently
skipping.
* fix: clamp summarizer default output budget by model metadata instead of adopting it
Model maxTokens is reported capability, not a product default: without an
explicit configuration the summarizer now requests the 4096 default, lowered
by model metadata when the model reports less, never raised by it. Explicit
values still win as-is.
* feat(vscode): remove YOLO mode setting, migrate old users to auto-approve all
The SDK extension's YOLO toggle was cosmetic: nothing in the approval
path read it, so runs were silently governed by the per-action
auto-approval settings underneath (cline/cline#13114). Instead of
keeping a parallel override system, remove the setting entirely and
make the auto-approve menu the single source of truth:
- drop yoloModeToggled (and the equally dead autoApproveAllToggled)
from state keys, settings handlers, state posts, telemetry, the
remote-config yoloModeAllowed transform, and the settings protos
(field numbers reserved)
- remove the Yolo Mode toggle from Settings -> Features (the whole
Experimental section, it was the only entry) and the
"Auto-approve: YOLO" AutoApproveBar takeover
- add a v3 storage migration that folds a previously-enabled YOLO /
auto-approve-all toggle into autoApprovalSettings by enabling every
action, so previously-unattended setups keep running unattended;
the dead keys are cleared from the file store
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* refactor(vscode): keep dead yolo keys in place instead of clearing them
Current builds never read the removed keys (the state loader only visits
known keys), so deleting them buys nothing - and the file store is shared
with older builds that still know them, so clearing would flip YOLO off
for a user who downgrades. Same downgrade-safety rule as the v1 export.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* chore(vscode): rename wasUnattended to shouldEnableAllActions in yolo migration
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* chore(vscode): drop dead toggleActModeForYoloMode and stale yoloModeAllowed comment
The method was a legacy-controller carryover nothing called, and it set
the mode without rebuilding the session, which is wrong for the SDK
architecture. The comment cited yoloModeAllowed as a live remote-config
example; it no longer maps to anything.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* chore(vscode): refresh checked-in proto descriptor_set.pb
The tracked descriptor set had not been regenerated since the repo
move and still advertised long-changed schemas (including the removed
yolo_mode_toggled fields) to gRPC reflection clients. Sync it with the
output of bun run protos.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: paste clipboard images into the composer as attachments (CLIENTS-78)
Pasting a screenshot into the composer did nothing: only drag-and-drop
and the paperclip file picker fed the attachment pipeline. Add an
onPaste handler on the composer textarea that extracts image files from
the clipboard, renames them to timestamped pasted-image-*.png files, and
routes them through the existing onAttachFiles flow. Text pastes are
untouched.
* desktop: only extract clipboard images in formats message serialization supports
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: context-aware welcome suggestions for non-code folders (CLIENTS-98)
* desktop: treat pending branch discovery as its own state for welcome cards
The welcome-card classifier read the "no-git" sentinel as a confirmed
non-repo, but page.tsx also used that value for the initial state and
while a workspace switch was awaiting branch discovery, so a git repo
could briefly show the plain-folder cards. Branch state is now null
while discovery is pending: the welcome screen shows no cards until the
folder is classified, and chat-mode cards (which never depend on git
state) still show immediately. Other branch consumers keep the string
contract via a "no-git" fallback.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: carry nullable branch state to all consumers
Propagate the pending-discovery null through ChatInputBar,
WorkspaceSelector, and the welcome workspace controls instead of
coercing to "no-git" at the page boundary, so only display leaves
fall back and the welcome classifier is the single consumer that
distinguishes pending from confirmed non-repo.
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: never let 'Add project…' fail silently; add manual folder path entry (CLIENTS-73)
- sidecar picker tries zenity then kdialog on Linux and throws a descriptive
error when neither exists, instead of returning null (indistinguishable
from user cancel); picked paths are trimmed of trailing separators
- picker failures now surface as visible error messages in both workspace
selectors, with a manual path-entry fallback (typed absolute or ~ paths
in the search box offer an 'Open folder' action)
- failed workspace switches (invalid/nonexistent paths) show an inline
error instead of silently doing nothing
- validate_workspace_directory expands ~ and returns the resolved path
* desktop: keep workspace menu search/error state through catalog refreshes
The welcome-screen workspace picker reset its search text and error
message whenever onRefreshWorkspaces changed identity, which happens on
every session-history poll. Typing a path or reading an inline error
raced against the timer: the menu would silently wipe mid-interaction.
Hold the refresh callback in a ref so the reset only runs when the menu
actually opens.
* desktop: format welcome-workspace-controls test
* desktop: distinguish picker launch failures from user cancellation
A zenity/kdialog rejection with a non-ENOENT spawn error (EACCES, EMFILE,
ENOMEM) or a crash signal was classified as a user cancel, which skipped
the kdialog fallback and suppressed the inline error - recreating the
silent no-op this branch is meant to eliminate. Only a clean exit code 1
from a dialog that actually opened now counts as cancellation; broken
backends fall through to the next candidate and surface a descriptive
error otherwise. Picker logic moved to sidecar/workspace-picker.ts with
an injectable exec so the classification is unit-tested.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Classify picker launch failures separately from user cancellation
zenity/kdialog failures like EACCES, EMFILE, or ENOMEM were treated as
user cancellation, suppressing the kdialog fallback and the inline
manual-entry error. Only a clean exit code 1 now counts as a cancel;
any other failure falls through to the next backend or throws the
picker-unavailable error. Picker logic moved to sidecar/folder-picker.ts
with an injectable exec so the classification is unit tested.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* Revert "Classify picker launch failures separately from user cancellation"
This reverts commit 24d27a004d.
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(core): keep a hung MCP server from taking down session creation
A stdio MCP server that never finishes initializing used to hold its
connect open for the full DEFAULT_MCP_CONNECT_TIMEOUT_MS (doubled across
the newline/framed attempts). MCP tool discovery runs on the
session.create critical path, so that wait blew past the 30s hub command
timeout and the CLI tore the whole interactive session down instead of
just skipping the bad server.
- Bound MCP tool loading during session build with a startup budget that
is safely under the hub command timeout. Servers that connect in time
contribute their tools; slower/hung servers are skipped for the session
(their error still surfaces via the MCP manager) instead of failing
session creation. Budget is overridable via CLINE_MCP_STARTUP_BUDGET_MS
for tests.
- Add StdioMcpClient.close() (and optional McpServerClient.close) that
marks the client disposed so an in-flight connect() aborts its retry
loop instead of respawning the framed fallback.
- Dispose the manager by closing clients up front, outside the per-server
operation locks, so a server hung in initialize can no longer stall
teardown for the full connect budget.
Adds regression tests covering both the non-blocking build and prompt
disposal while a client is hung in connect().
* refactor(core): simplify hung-MCP-server fix to a startup budget
Replace the bespoke per-server race/tracking in loadConfiguredMcpTools
with a small withStartupBudget() wrapper around the existing
Promise.allSettled: a server that exceeds the budget becomes a normal
rejection that the existing loop already logs and skips. The connect
budget, MCP settings display (initialize timeout 30s), and the rest of
the loader are left untouched.
The client close()/manager.dispose() cleanup is kept minimal: it is what
lets teardown abort a still-in-flight connect instead of blocking on the
per-server lock (and clears the pending request timer).
* fix(mcp): cap the default initialize budget at 3s to protect session creation
Supersedes the startup-budget approach on this branch with the simple
constant fix.
MCP initialize runs on the session.create critical path, which the hub
caps at 30s, and connect() can spend the budget twice (newline then
Content-Length framing). The 30s default from #13067 meant a server that
never initializes held session.create for up to 60s, so the hub RPC
timed out and the CLI tore the whole session down and exited.
Return to the pre-#13067 shape with a bigger probe: 3s instead of 1.5s.
That still covers the ~2s starters the old probe killed (#13035) and
keeps the worst case at ~6s per server, far under the hub deadline.
Genuinely slow starters (JVM-based servers like Oracle SQLcl) now need
an explicit timeout in cline_mcp_settings.json, which continues to
override the default in either direction.
Tests: update the slow-start regression tests to the new policy (2s
connects by default, 4s connects with a configured timeout), refresh the
displayed initialize-timeout assertions, and add an invariant test that
keeps the doubled default well under HUB_DEFAULT_COMMAND_TIMEOUT_MS so
the budget cannot silently creep past the session deadline again.
* fix(desktop): stop opening a session from replacing the remembered model
The composer's ModelSelector mirrored every provider/model prop change
into the remembered last selection (localStorage), which seeds new
sessions via getInitialChatConfig(). Opening an existing session drives
those props to that session's config, so merely viewing an old session
silently replaced the user's explicitly picked default model.
The remembered selection is now written only from the explicit picker
handlers (provider select and model select). Passive prop changes, such
as opening a session, no longer touch it.
* fix(desktop): re-seed remembered provider/model on chat reset
reset() kept the previous config's provider/model and only cleared the
session ID, so a chat pane that had hydrated a historical session could
carry that session's model into the next chat. Re-seed provider/model
(and apiKey when the provider changes) from the remembered defaults --
the same source a freshly mounted thread uses -- so reset and remount
behave identically.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(desktop): stop treating leftover plugin install dirs as installed
isOfficialPluginInstalled() only checked that the marketplace install
directory existed. A failed or interrupted install can leave that
directory behind with no plugin inside, and the next install attempt
then short-circuited with a fake 'already installed' success: the
marketplace button flipped to Uninstall with no error while nothing
actually worked, and the installed-entries listing kept reporting the
broken entry as installed.
The check now requires a loadable plugin module inside the directory
(via discoverPluginModulePaths) before reporting the entry as
installed, so partial directories fall through to a real install
attempt whose outcome is surfaced to the UI.
* fix(desktop): reclaim leftover partial plugin install dirs with --force
* fix(desktop): canonicalize diff panel paths against the session cwd
Tool calls address the same file inconsistently across a session: one
edit uses a workspace-relative path (journal.txt), a later one the
absolute path (/tmp/ws/journal.txt). mergeToolDiffs keyed entries by the
raw string, so the same file was listed twice in the diff panel with
split +/- counts and inconsistent naming, most visibly after git was
initialized mid-session and the model switched to absolute paths.
Diff paths are now canonicalized against the session cwd before
merging: entries for the same file collapse into one, files inside the
cwd display as workspace-relative paths, and files outside it display
their resolved path. Without a cwd the previous raw-key behavior is
kept.
* style: collapse editorReplaceEvent signature per biome format
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(desktop): collapse dot segments and keep root cwd in diff path keys
* fix(desktop): compare Windows diff path keys case-insensitively
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(vscode): hide View Changes on completion rows until there are changes to show
The button previously always rendered on the latest completion row, faded
and disabled when the count check came back 0 - which covers both 'nothing
changed since your last message' and 'no checkpoint to compare against'
(non-git workspace, repo with no commits, comparison failure). A dead
button with a misleading tooltip in the non-git case is worse than no
button: now the row renders nothing until the host confirms there are
actual changes, and the button is always enabled when shown.
* fix(vscode): reset View Changes state when showViewChanges toggles
Greptile review: a stale positive hasChanges from a previous evaluation
could flash the button before the host confirms the new comparison when
showViewChanges flips false and back true on the same row. Reset to
'still checking' whenever the effect re-runs.
* fix(cli): render MCP tool result text instead of escaped JSON in TUI
MCP tools return {content: [{type: "text", text}]} which
extractFullOutputText JSON-stringified, escaping newlines into one giant
line that word-wrapped across the whole terminal and never triggered the
line-based collapse. Extract the text parts with real newlines so the
existing collapse works.
Fixes#13038
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(cli): keep placeholders for non-text blocks in mixed MCP results
Addresses Greptile review on #13066: text-only filtering silently
dropped image/resource/audio blocks from mixed MCP content. Render them
as [type] placeholders instead.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(cli): surface non-text MCP block metadata in TUI output
Extract embedded resource text, and include resource/resource_link URIs
and image/audio mime types in placeholders so expanded mixed MCP
results keep identifying metadata.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
The stdio MCP client gave servers without a configured `timeout` only
1.5 seconds to answer initialize before killing the process, so
slow-starting servers (e.g. Oracle SQLcl's JVM-based `sql -mcp`) could
never load and were silently skipped at session start.
Raise the default connect budget to 30s, in line with the startup
budget other MCP clients allow. A configured `timeout` still overrides
it in either direction, dead commands still fail fast through the spawn
error/exit path, and the newline -> Content-Length framing fallback is
unchanged.
Fixes#13035
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* feat(desktop): route /team prompts through core runtime
Rewrite desktop `/team` commands as structured user command blocks before sending them to the core runtime. Validate task input and respect the globally disabled Teams tool setting.
Remove legacy agent spawn and team enablement flags from session configuration, and add coverage for prompt rewriting and disabled-tool behavior.
* fix(desktop): preserve team tool defaults
* fix(desktop): display queued /team prompts as their slash form
Queued prompts are stored in their runtime form, so a queued /team
command showed its raw <user_command> envelope in the prompt queue chip
and edit textarea. Fold queue items through formatDisplayUserInput for
display; saving an edit re-resolves the slash form through the sidecar,
so the round trip is lossless.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* chore(hub): align builtin tool catalog flags with the desktop sidecar
The desktop sidecar pins enableSpawnAgent/enableAgentTeams when listing
the builtin tool catalog; the hub's parallel listing did not, so the two
would drift if the preset defaults ever change. Pin the same flags in
the hub and cross-reference the two call sites.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* chore(desktop): drop inert enableSpawn/enableTeams config leftovers
buildCoreSessionConfig no longer reads these keys, so remove the dead
schema fields, default-config initializers, and chat-test payload
entries. The chat-session regression test still sends them on purpose
to prove legacy flags cannot override the runtime's tool presets.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(desktop): reject /team when the mode's tool preset disables teams
The /team guard only checked the global disabled-tools setting, but the
runtime resolves tool availability from the mode's preset, so a preset
without team tools (yolo) would still send the model a spawn-a-team
instruction it cannot act on. Resolve the teams catalog entry for the
session's mode and reject /team when it is unavailable, mirroring the
runtime's own availability logic.
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>
Toggling Plan/Act while a turn was streaming or waiting on a tool approval
aborted the turn but left the TurnStateTracker on its last live phase: the
aborted session's done event is fenced off as stale once the rebuild
unsubscribes it, so nothing ever settled the phase. The webview then kept
rendering that phase forever - an eternal Thinking spinner with the input
disabled (aborted while streaming), or dead Approve/Run Command buttons wired
to an approval that clearPending had already denied (aborted while awaiting
approval). Users experienced this as 'switched to act mode and nothing
happened / it never wrote the files'.
Mirror cancelTask: after aborting the turn for the mode change, append a
resume_task ask row and set the phase to resumable, so the footer offers
Resume Task with the input enabled in the new mode.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: native-feel polish and render-path performance fixes
- Suppress the WebView browser context menu on app chrome (keep it for
editable fields and active text selections)
- Make UI chrome unselectable app-wide; opt chat messages, markdown,
code, diffs, and error banners back into text selection
- Contain overscroll so inner scrollers don't rubber-band the window
- Lazy-load Settings/Sessions/Onboarding/Diff views out of the entry chunk
- Memoize ChatInputBar and AgentHeader; stabilize their props in the chat
pane so stream flushes only re-render the affected message bubble
- Stop refocusing the composer textarea on every keystroke (caret flicker)
- Cache slash commands across menu opens (stale-while-revalidate)
- Avoid rebuilding reversed message arrays and ask-question JSX per render
- Drop core info/debug console logging on the streaming hot path behind a
cline:debug-logs opt-in; remove leftover [webview:delete] debug logs
- SearchCombobox (provider/model picker): Escape closes and restores focus
- Remove unused @vercel/analytics, recharts, embla-carousel deps and the
unused chart/carousel UI components
* desktop: surface failed-turn errors instead of leaving the chat blank
On a failed run the runtime reports its error string in result.text.
The webview rendered that as an assistant bubble, which the canonical
history rehydration then wiped (the failed turn is never persisted),
so provider errors like a retired model id left the user staring at a
silently empty chat. Route failed-turn text to a persistent error-role
message added after rehydration instead.
* desktop: fade the welcome/conversation swap instead of hard-cutting
Sending the first message replaced the hero layout with the message
grid in a single commit, which read as a white flash. A 180ms enter
animation now plays when either side becomes visible; disabled under
prefers-reduced-motion.
* desktop: render new-chat panes instantly from the last catalog load
Clicking + remounts ChatThreadPane, which refused to render until the
provider catalog (a large fetch) and workspace list resolved again —
about a second of blank pane plus boot spinner on every new chat.
Seed remounts from a module-level snapshot of the last successful
load; the mount effect still refreshes both in the background.
* desktop: invalidate the provider-catalog snapshot with the cache
Seeding remounted chat panes from the last catalog load left a window
where a pane created right after a credential change could act on the
old keys. The snapshot now lives in the catalog module and is dropped
by invalidateProviderCatalogCache(), so credential edits force the
next remount to wait for fresh data.
* fix(cli): harden tool input/output formatters against malformed payloads
Tool inputs cross the model/tool boundary and may not match their
TypeScript annotations (e.g. run_commands with { command: null }).
truncate() called str.replace() on such values, crashing the TUI with
'.replace is not a function' and making persisted sessions containing
the payload non-resumable, since hydration replays the same input
through formatToolInput().
Normalize untrusted values at the formatting boundary: truncate() now
accepts unknown and safely stringifies null/undefined/objects (including
circular structures and throwing toJSON), formatStructuredCommand no
longer returns non-string commands verbatim, and fetch_web_content
request summaries tolerate malformed entries.
Fixes#13036
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(cli): keep valid empty-string args in structured command summaries
Greptile review: filtering normalized args by truthiness also dropped
genuine empty-string argv entries, so summaries could show a different
argument list than the one executed. Filter only nullish entries before
normalization instead.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* fix(vscode): fall back to session cwd/Desktop for @-mention search in empty windows
* fix(vscode): use the shared chat workspace as the no-folder fallback root
* fix(desktop): treat signed-out state as a typed result instead of a command error
* fix(desktop): sign out when the organization balance fetch reports the typed signed-out result
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* chore: retrigger checks after runner outage
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* refactor(ui): introduce Cline-owned semantic color system
* refactor(desktop): adopt shared semantic theme roles
* refactor(ui): set 15px root and recalibrate xs/sm type scale
Scale rem steps so xs/sm stay 12/13px visually, and slightly lift dark-mode neutral-4.
* refactor(ui): align SearchCombobox with package type and hover tokens
Use host-safe cline-ui utilities and keep option font inheritance from CSS.
* fix(ui): use standard stroke-2 utility on approval spinner
* refactor(desktop): modernize shared UI primitives for Tailwind v4
Replace legacy arbitrary/has selectors with current utility syntax.
* refactor(desktop): bump chat chrome typography to text-sm
Keep composer controls and pickers on the shared sm type step.
* refactor(desktop): use max-w-344 for page frame content width
* chore(desktop): disable Next.js dev indicators
* chore: ignore desktop-app Cursor settings
* docs(pr): add before/after screenshots for #12941
* chore: retrigger checks
---------
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
* desktop: fix silent turn failures, message duplication, and stuck composer; add first-run setup guidance
Findings from two full computer-use UX audits of the desktop app:
- Surface failed turns in the transcript: queued turns (incl. the first
prompt of a fresh session) only signal errors via chat_done, which the
UI previously ignored - sending a message with no credentials failed
in complete silence. Failed turns now show an error message enriched
with the latest core error log and a pointer to Settings -> Models.
- Fix duplicated user messages: a live send's optimistic user message
was materialized a second time by the runtime's queued-prompt-start
event.
- Fix composer stuck on 'Agent is working...': drop prompts from the
local queue snapshot when they start, emit a fresh queue snapshot from
the sidecar on pending_prompt_submitted, and double-check the server
queue on turn completion.
- Add a 'Connect a model' notice on the welcome screen when no provider
has credentials, with actions to reopen onboarding at the connect step
or jump to model settings; it reacts live to credential changes.
- Add 'Get an API key' links for popular providers in onboarding and
Settings -> Models (the catalog docUrl is never populated), and link
the Cline dashboard from the Cline API key form.
- Explain what Cline is on the onboarding welcome step.
- Make the stop button visible (was 8px with no padding) and support
Esc to stop; add Cmd/Ctrl+N (new session) and Cmd/Ctrl+, (settings).
- Remove leftover [webview:delete] console.error debug logging that
surfaced an error badge after deleting a session.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: remove remaining delete debug logging in session history hook
The sidebar right-click delete path had the same leftover [webview:delete]
console.error instrumentation, which made the Next dev-mode issues badge
appear after every deletion.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: fix Biome a11y error in WelcomeSetupNotice
biome's lint/a11y/useSemanticElements errors on role="status" divs;
use the semantic <output> element (implicit status role) instead. This
was failing the repo's 'bun run lint'.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: count structured-config and keyless providers as connected
The welcome setup notice previously only recognized apiKey/OAuth
credentials, so users running Bedrock/Vertex (structured configValues)
or a deliberately enabled keyless local endpoint (e.g. Ollama) were
nagged to connect a model they already use. isProviderConnected now
also counts an enabled provider whose required config fields are all
filled, or an enabled provider that has no API-key field at all.
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* desktop: keep re-key eligible when chat_done lands in the same batch as its prompt start
When a turn fails fast, chat_queued_prompt_start and chat_done can be
dispatched in one React batch. Clearing the outstanding-optimistic-
bubble registry synchronously in the chat_done handler ran before the
re-key updater enqueued by the prompt-start event, so the optimistic
bubble was appended a second time instead of re-keyed. Clear the
registry inside a state updater so it executes in event order after
the re-key. Caught by the queued-turn-failure regression test.
* desktop: make the queued-prompt re-key updater idempotent under StrictMode
React StrictMode double-invokes state updaters in dev. The
chat_queued_prompt_start re-key updater consumed the optimistic
bubble's id from outstandingOptimisticUserIdsRef on its first run, so
the second run against the same prev found no eligible candidate and
appended the same user message a second time (and, without a promptId,
makeId() minted a different id per invocation). Hoist the message id
out of the updater and remember which optimistic bubble each queued
message id re-keyed so a re-run reaches the identical result. The memo
resets alongside the outstanding set (error state, reset, hydration).
Root-caused with runtime instrumentation: the duplicate only appeared
on turns that exercised the queue-drain re-key path, and hydration
later collapsed it to one message because the duplicate never existed
in persisted state.
* desktop: preserve failure messages across post-send canonical hydration
Persisted history never contains UI-only error bubbles, so the two
post-send read_session_messages replacements in sendPrompt wiped the
failure explanation appended from chat_done ~40ms after it rendered
(confirmed with runtime instrumentation). Re-append the active
session's error messages after the canonical history. Includes a
regression test reproducing the chat_done-error-then-RPC-resolution
race.
* desktop: don't let an optional API-key field veto a connected provider
Greptile P1 follow-up: Bedrock's catalog entry carries an optional
apiKey field ('Optional Bedrock bearer token') alongside IAM/profile
authentication, and keyless local endpoints can also surface one — so
treating the mere presence of an apiKey field as proof of disconnection
kept nagging configured users. An enabled provider (the user
deliberately persisted settings for it) now counts as connected unless
a required config field is unmet; auth may legitimately live outside
the catalog (IAM, env vars, local endpoints). Brand-new users have no
enabled providers, so the first-run notice still shows for them.
* desktop: tighten credential-error guidance and stop re-pinning stale failure bubbles
* desktop: invalidate the shared provider catalog after settings OAuth login
Greptile P1 follow-up: runOAuthProviderLogin only updated the settings
view's local provider state, so the shared catalog cache and its
invalidation subscribers (the composer selector and the welcome
screen's 'Connect a model' notice) kept reporting the provider as
disconnected until an unrelated invalidation or a pane remount. Notify
the shared cache on successful OAuth login, like the account view and
the API-key save path already do.
* desktop: clear the remembered core error on turn end, reset, and hydration
Greptile flagged that turn-start events are the only thing clearing
lastCoreErrorBySessionRef, and websocket events are not replayed: a
transport interruption that drops a turn's start event lets a later
detail-less failure resurrect an earlier turn's error. The remembered
error belongs to exactly one turn, so clear it whenever a turn ends
(chat_done, any reason) as well as on reset() and history hydration.
Regression test covers the dropped-start-event sequence.
---------
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
* feat(vscode): explain when a free model promotion ends
Once a free promotion ends, the cline-free/ model is removed from the
catalog and the backend answers 'model not found' to requests against it.
The CLI has shown a dedicated 'Free model promotion ended' banner for this
since #12593; the extension instead rewrote the answer into generic
model-not-found guidance with no model-picker offramp.
Detect the case in the host where the active model id is known
(reshapeErrorForWebview, fed by a new MessageTranslatorState model-id
source), stamp the payload with a cline_free_promotion_ended code, and
render a dedicated card in the webview with a button into the model
picker. Classification is gated on the cline-free/ prefix so ordinary
model-not-found errors keep their generic path, and it runs before the
auth branch since the 404 status falls inside the generic auth range.
* fix(vscode): prefer the live task model over session-start metadata
A mid-task model-only switch updates the running session's model in place
(updateActiveSessionModel) and refreshes the task API shim, but never
touches the session's startConfig/manifest. Preferring the session-start
snapshot could therefore misclassify after such a switch: a genuine
retired-model 404 would miss the promotion-ended card, and the reverse
switch could show it for the wrong model. Provider switches restart the
session, so both sources agree there; the shim starts as "unknown"
(filtered out), so fresh sessions still resolve through start metadata.
* fix(desktop): dedupe chat_queued_prompt_start emitted for the same prompt
PendingPromptService.drain() emits a pending_prompts snapshot (head
removed) and a pending_prompt_submitted event back-to-back for the same
prompt. The sidecar translated both into chat_queued_prompt_start, so
the webview rendered the user's message twice until the chat was
re-hydrated from history. Track the last announced prompt id per live
session and emit the start chunk once.
* fix(desktop): re-key optimistic user bubble when the runtime queues the prompt
The send path renders an optimistic user bubble for prompts dispatched
while the session is idle, keyed by a random id. When the runtime
routes that prompt through its pending queue (e.g. during session
startup), the queued-prompt-start event appended a second bubble under
queued_user_<promptId> — the same message rendered twice until the
chat was re-hydrated from history. Re-key the trailing optimistic
bubble to the event's id instead of appending.
* fix(desktop): re-key only outstanding optimistic bubbles on queued prompt start
Review follow-up: matching by content alone could swallow a new queued
prompt that repeats the text of a message left at the transcript tail
by an earlier cancelled/failed turn. Track in-flight optimistic bubble
ids explicitly (registered on optimistic append; cleared on re-key,
turn end, error, and history hydration) and only re-key those.