Compare commits

..
Author SHA1 Message Date
Saoud Rizwan 396032cd3b chore(cli): release v3.0.45 2026-07-18 21:03:37 -07:00
Saoud Rizwan f33ab3a872 chore(sdk): release v0.0.65 2026-07-18 20:46:28 -07:00
Saoud Rizwan 2ca8364ffc docs: add Kimi K3 to ClinePass model list and reference pricing (#12393) 2026-07-18 20:35:48 -07:00
Saoud Rizwan 2ef81be703 feat(llms): make Claude Code and Codex provider packages optional peers (#12379)
ai-sdk-provider-claude-code and ai-sdk-provider-codex-cli were hard
dependencies of @cline/llms, so every npm install of the cline CLI
pulled their native binaries (~250MB claude-agent-sdk platform binary,
~105MB @openai/codex) even for users who never select those providers.

Move both to optional peerDependencies (kept as devDependencies so
monorepo builds still bundle the JS) and load them via literal dynamic
imports in community.ts, mirroring the existing opencode-sdk pattern.

The Claude Code provider now resolves the claude executable explicitly:
bundled platform package when present, otherwise a user-installed
claude from PATH, passed via defaultSettings.pathToClaudeCodeExecutable.
The agent SDK's own resolution cannot be used from Bun-compiled
binaries because it anchors on the virtual bunfs where node_modules
lookups never see packages on disk. Codex already degrades gracefully
(npx -y @openai/codex, then codex on PATH).
2026-07-18 20:30:24 -07:00
Saoud Rizwan 359445ae0c fix(sdk): stop exposing the team spawn tool to teammates (#12371)
* fix(sdk): retry runs once after refreshing expired OAuth credentials

Teammate and subagent sessions inherit the OAuth access token as a
snapshot at spawn time and had no refresh path: when the token expired
while the lead was blocked (e.g. in team_await_runs), their next model
call died with the provider's raw 401 body. Only the lead's turn-start
sync and runWithAuthRetry could refresh, and neither runs mid-turn.

Add an onAuthError hook to AgentConfig, wired once per session by
LocalRuntimeHost: it refreshes credentials through the shared
single-flight RuntimeOAuthTokenManager and propagates the new key to
the lead, delegated defaults, and all teammates via the existing
updateConnection channels. SessionRuntime retries a run once when it
failed with an auth-like error and the refresh succeeded, continuing
from the persisted trail so completed iterations aren't replayed.

Also fix isLikelyAuthError to lowercase string inputs; the server's
'Unauthorized: ...' message only matched when wrapped in an Error.

* fix(sdk): report errored teammate runs as failed instead of completed

Model-stream failures return results with finishReason 'error' rather
than throwing, so executeQueuedRun marked such runs 'completed' with
the error buried in resultSummary. Throw into the existing failure
path so the run reports status 'failed' (with run.error set and a
RunFailed event) and the retry machinery engages when maxRetries
allows.

* fix(sdk): stop exposing the team spawn tool to teammates

Spawning is lead-only, enforced at execution time, so teammates that
saw team_spawn_teammate in their toolset burned turns on 'Only the
lead agent can manage teammates.' rejections before falling back to
doing the work themselves.
2026-07-18 20:21:35 -07:00
Saoud Rizwan d9e2e9c76b fix(sdk): report errored teammate runs as failed instead of completed (#12370)
* fix(sdk): retry runs once after refreshing expired OAuth credentials

Teammate and subagent sessions inherit the OAuth access token as a
snapshot at spawn time and had no refresh path: when the token expired
while the lead was blocked (e.g. in team_await_runs), their next model
call died with the provider's raw 401 body. Only the lead's turn-start
sync and runWithAuthRetry could refresh, and neither runs mid-turn.

Add an onAuthError hook to AgentConfig, wired once per session by
LocalRuntimeHost: it refreshes credentials through the shared
single-flight RuntimeOAuthTokenManager and propagates the new key to
the lead, delegated defaults, and all teammates via the existing
updateConnection channels. SessionRuntime retries a run once when it
failed with an auth-like error and the refresh succeeded, continuing
from the persisted trail so completed iterations aren't replayed.

Also fix isLikelyAuthError to lowercase string inputs; the server's
'Unauthorized: ...' message only matched when wrapped in an Error.

* fix(sdk): report errored teammate runs as failed instead of completed

Model-stream failures return results with finishReason 'error' rather
than throwing, so executeQueuedRun marked such runs 'completed' with
the error buried in resultSummary. Throw into the existing failure
path so the run reports status 'failed' (with run.error set and a
RunFailed event) and the retry machinery engages when maxRetries
allows.
2026-07-18 20:19:17 -07:00
Saoud Rizwan d859a86a6f fix(sdk): retry runs once after refreshing expired OAuth credentials (#12369)
* fix(sdk): retry runs once after refreshing expired OAuth credentials

Teammate and subagent sessions inherit the OAuth access token as a
snapshot at spawn time and had no refresh path: when the token expired
while the lead was blocked (e.g. in team_await_runs), their next model
call died with the provider's raw 401 body. Only the lead's turn-start
sync and runWithAuthRetry could refresh, and neither runs mid-turn.

Add an onAuthError hook to AgentConfig, wired once per session by
LocalRuntimeHost: it refreshes credentials through the shared
single-flight RuntimeOAuthTokenManager and propagates the new key to
the lead, delegated defaults, and all teammates via the existing
updateConnection channels. SessionRuntime retries a run once when it
failed with an auth-like error and the refresh succeeded, continuing
from the persisted trail so completed iterations aren't replayed.

Also fix isLikelyAuthError to lowercase string inputs; the server's
'Unauthorized: ...' message only matched when wrapped in an Error.

* feat(telemetry): emit user.auth_run_retry when a run is retried after credential refresh

Addresses Greptile review on the auth-retry PR: the refresh itself was
already instrumented (auth_refresh_soft_failure / auth_logged_out fire
inside getValidClineCredentials), but the new retry transition was not.
The recovered flag counts runs that would previously have died with the
raw provider 401 — the direct production measure of this fix working.
2026-07-18 20:08:18 -07:00
Saoud Rizwan 0b7b9c1b3d fix(llms): add Kimi K3 to bundled ClinePass model fallback (#12392)
* fix(llms): add cline-pass/kimi-k3 to bundled model catalog fallback

* fix(llms): derive cline-pass default model from catalog authored order

Adding kimi-k3 (newest releaseDate) to the bundled cline-pass catalog
would have flipped firstGeneratedModelId — which sorts by release date —
to cline-pass/kimi-k3, silently changing the default model for new
ClinePass setups. Use the catalog's authored order instead, which mirrors
the recommended-models endpoint's curated order (intended default first,
subscription models before free ones).
2026-07-18 20:03:19 -07:00
Dominic Cooney 557d725690 fix(vscode): shell mismatch between prompt, execution, and user configuration on Windows (#12331)
* Rationalize shell identification and prompting, especially on Windows.

* Probe all pwsh install locations for the Windows default shell.

The default-shell fallback only checked the Program Files pwsh path,
so Microsoft Store installs of PowerShell 7 fell back to Windows
PowerShell while VS Code's own terminal launched pwsh. Share one
candidate list between the sync default-shell check and the async
PowerShell prober. Also drop an 'as string' cast that hid the
setting's type from the checker.

* Address shell resolution review feedback

* Resolve array-valued terminal profile paths on macOS and Linux too

VS Code permits terminal profile 'path' to be string | string[] on every
platform, not just Windows. The resolver (env expansion, first-existing
selection, PATH lookup) is now platform-generic: it uses the host path
module's separators and delimiter, probes PATHEXT only on Windows, and
treats env var names case-insensitively only on Windows. The macOS and
Linux getters route through it instead of returning the raw config value,
which crashed getShellKind() for array values.

* Apply terminal profile changes at the model-request boundary

A terminal profile change previously triggered a deferred session rebuild
to refresh the run_commands tool description. While a task was running the
rebuild waited, so the description could name one shell while commands
executed in another for the rest of the turn.

Instead of rebuilding, createShellTool now accepts a shell provider
function and re-derives the description each time the runtime reads it,
which happens exactly when a model request is built. The VS Code tool
snapshots {profileId, shell} in that provider; both execution paths (the
background spawn and the foreground terminal, via a new profile parameter
on getOrCreateTerminal) consume the snapshot. Commands produced by an
in-flight inference therefore run with the shell the model was told about,
and a mid-turn profile change takes effect when the tool results are sent
back: the next request names and uses the new shell.

The profile-change session rebuild path (handleTerminalProfileChanged) is
removed along with its deferred-rebuild window.

* Use the real createShellTool in the vitest @cline/core stub

The stub's hand-rolled createShellTool duplicated the 'shell must be a
string' invariant instead of exercising the code that enforces it
(getShellKind via description building), so the array-valued-profile
regression test proved only that the stub threw, not that the real tool
survives. Re-export the real implementation from SDK source — the same
pattern the stub already uses for the apply-patch and editor executors —
and assert on the actual generated descriptions, including that a profile
change is reflected at the next description read.

* Harden shell profile path resolution edge cases

- Warn and skip profile paths containing variable references beyond
  \ (e.g. \) instead of silently probing a
  literal path that can never exist; later candidates and the platform
  default still apply.
- Document that an overriding bash executor in createBuiltinTools bypasses
  the resolved canonical shell and must honor it to keep the run_commands
  description truthful.
2026-07-18 04:23:00 +02:00
Saoud Rizwan 7274d8badc feat(ui): add agent chat components, Storybook, and npm releases (#12374)
* feat(ui): add shared agent chat components and Storybook

* ci(ui): add standalone npm publishing

* docs(ui): keep release commands environment-neutral

* refactor(ui): simplify package validation

* refactor(ui): tighten package and release contracts

* docs(ui): remove duplicate install guidance

* ci(ui): make publishing workflow manual-only
2026-07-17 18:22:57 -07:00
Bee d1837366c0 chore(llms): update model catalog (#12366)
Update model catalog with bun run build:models
Version updated to 1784318695007
2026-07-17 13:28:59 -07:00
Saoud Rizwan c380daf4a3 docs(ui): add adoption primer (#12367) 2026-07-17 13:21:15 -07:00
Bee c564045d81 chore(cli): includes version numbers in hub status output (#12358)
Includes version numbers in hub status output and doctor command to make debugging with user easier.
2026-07-17 05:37:54 +02:00
Saoud Rizwan 9a5e1751b2 chore(cli): release v3.0.44 2026-07-16 18:38:54 -07:00
Saoud Rizwan 131e25e1a1 chore(sdk): release v0.0.64 2026-07-16 18:14:58 -07:00
Saoud Rizwan a7ff007af9 chore(cli): release v3.0.43 2026-07-16 17:52:46 -07:00
ef27f45080 fix: max output token handling (#12031)
* fix: max output token handling

* shared

* max reasoning budgetTokens

* fix unit test

* fix: address review feedback on max output token handling

- OpenRouter effort branch sends only reasoning.effort (OpenRouter rejects
  effort combined with reasoning.max_tokens)
- OpenAI Responses forwards explicit caller maxTokens for API-key usage;
  ChatGPT OAuth and synthesized gateway defaults are still omitted
- Gateway lifts the synthesized default output cap above explicit Anthropic
  reasoning budgets so max_tokens > thinking.budget_tokens holds

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address second-round review feedback on max output token handling

- Replace the gateway-only requestedMaxTokens field with a defaultedMaxTokens
  flag set when the gateway synthesizes a cap, so explicit maxTokens from
  direct provider callers is forwarded by default (greptile P1)
- Check the parsed hostname instead of a URL substring when detecting the
  ChatGPT OAuth backend (CodeQL)
- Drop the empty else-if branch in toAiSdkMessages in favor of an explicit
  emptiedByDroppedReasoning condition (greptile P2; biome rejects the
  suggested bare continue)
- Dedupe isPositiveFiniteNumber by exporting it from gateway.ts (greptile P2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: extract isPositiveFiniteNumber into providers/utils.ts

Move the shared helper to its own module as suggested in review instead
of exporting it from gateway.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: remove unrelated VS Code changes

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-16 17:50:22 -07:00
Dominic Cooney e3c6d51072 fix: recognize frontmatter with a leading UTF-8 BOM (#12277)
* fix(vscode): recognize SKILL.md frontmatter with a leading UTF-8 BOM

SKILL.md files saved with a UTF-8 BOM (e.g. by Windows Notepad's 'UTF-8 with BOM' encoding) were silently skipped and not recognized as skills, because gray-matter/regex-style frontmatter parsers require '---' at byte offset 0 and never accounted for the leading BOM byte sequence Node's utf-8 decoder does not strip.

Fixes the shared parseYamlFrontmatter() helper (used by skills, rules, workflows, and remote skill entries in the VS Code extension) and every duplicated ad-hoc frontmatter regex across the SDK/CLI/hub/desktop-app/example-plugin code paths to strip a leading BOM before matching.

Adds regression tests exercising the exact reported scenario (BOM-prefixed SKILL.md silently missing name/description) in frontmatter.test.ts, skills.test.ts, skill-frontmatter-toggle.test.ts, user-instruction-config-loader.test.ts, and configured-agent-config.test.ts.

Fixes https://github.com/cline/cline/issues/12151

* refactor(shared): centralize UTF-8 BOM stripping

* refactor(shared): add UTF-8 file readers

* docs: guide UTF-8 configuration reads
2026-07-17 09:37:20 +09:00
Saoud Rizwan 48bac25548 chore(sdk): regenerate lockfile for v0.0.63 2026-07-16 17:13:44 -07:00
Saoud Rizwan 37f5f104f3 chore(sdk): release v0.0.63 2026-07-16 16:47:40 -07:00
Saoud Rizwan 3577b52404 feat(core): emit mistake-limit telemetry from the session runtime (#12355)
Moves the task.mistake_limit_reached capture (#12354) from the VS Code
SdkController wrapper into @cline/core so every host (CLI, VS Code,
hub daemon) emits it via its session telemetry service.

The MistakeTracker gains an onLimitTelemetry hook fired exactly once
per limit hit, before the limit decision is resolved — including when
no onConsecutiveMistakeLimitReached callback is configured (the
default-stop path, which the extension-side capture missed). The
orchestrator wires the hook to captureMistakeLimitReached using its
reserved telemetry field, reading sessionId/modelId/providerId at fire
time so mid-session connection updates are reflected.

The now-redundant extension wrapper and TelemetryService method are
removed to avoid double-counting in VS Code.
2026-07-16 16:39:50 -07:00
1843bc8ed0 fix(vscode): persist selected account organization (#12345)
* fix(vscode): persist selected account organization

* fix(vscode): ignore stale organization responses

---------

Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-16 16:08:07 -07:00
fead00ec57 fix(vscode): avoid duplicate OpenAI provider settings (#12346)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-16 16:07:48 -07:00
238107d21c fix(vscode): preview auto-approved apply patches (#12349)
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-16 16:05:24 -07:00
Saoud Rizwan 2063a661bd feat(vscode): capture telemetry when consecutive mistake limit is reached (#12354) 2026-07-16 15:58:21 -07:00
Dominic Cooney ec02d5862e Fix debug harness: run under node, drop ws dependency. (#12319)
The harness rotted after the npm-to-bun migration: the 'ws' package it imported is no longer in the dependency tree, and Playwright's _electron.launch() times out under bun (the debugee Electron starts but Playwright never finishes attaching; the same launch attaches in under a second under node). Use the runtime's built-in WebSocket for the CDP client and document that the harness must be run with node.
2026-07-17 07:43:32 +09:00
Mikołaj Kondratek 8452084842 fix: auto-discover OS trust anchors in the CLI wrapper (#11498)
* fix: auto-discover OS trust anchors in the CLI wrapper

The 3.x CLI ships as a Bun-compiled binary. Bun does not read the OS
trust store unless NODE_USE_SYSTEM_CA is set, and even with the flag its
Windows enumeration covers only the `Root` store, not `CA`/Intermediate
(verified empirically across the CLINE-2353 Windows repro rounds). So a
corporate MITM root is not trusted out of the box and inference fails
with "unable to get local issuer certificate". The pre-3.0 (Node) CLI
had no app-level CA handling either; users only succeeded by setting
NODE_EXTRA_CA_CERTS manually. The reporter's ask: have it just work
without the env var.

This follows the CLINE-2353 SDK fetch-threading change. That made the
inference client honor a host-provided proxy/CA-aware fetch, but on the
CLI Bun's global fetch is already proxy-aware and a fetch function
cannot cross the hub-daemon process boundary, so the CLI's missing piece
is trust material, not the fetch. Env vars do inherit across spawns.

The npm `bin/cline` wrapper runs on Node (not Bun), so it can read the
full OS store via tls.getCACertificates("system") (Node >= 22, no flag
required) — including the Windows `CA` store Bun skips — and hand the
certs to the Bun child via NODE_EXTRA_CA_CERTS, which both runtimes
honor. This mirrors the JetBrains plugin's configureCertificates(),
replacing "harvest from the IDE trust store" with "harvest from the OS".

The merge logic lives in a dependency-free, injectable-module CommonJS
helper (bin/ca-certs.cjs) so it is unit-testable and ships verbatim in
the generated wrapper package (publish copies bin/ wholesale). A
user-set NODE_EXTRA_CA_CERTS is merged ahead of the system certs; a
self-reference to the managed bundle is detected to avoid re-appending
every launch; when no system certs are available the user's setting is
left untouched. Writes are atomic (temp + rename) and owner-only.

Adds ca-certs.test.ts (13 cases) covering harvest filtering, user-bundle
PEM/DER/missing handling, newline-separated merge, managed-path
self-reference, and the no-system-certs no-op.

* fix: harden CLI auto-CA harvesting (review follow-ups)

Follow-ups from the CLINE-2353 review of the CLI auto-CA wrapper.

- H1: a legacy NODE_EXTRA_CA_CERTS set to an OS-path-delimited list
  ("a.pem;b.pem", the CLINE-2324 footgun Node never split) was stat'd as
  one file, failed, and silently dropped the user's certs. readUserCerts
  now tries the whole value as one file first, then splits on the OS path
  delimiter and reads each existing PEM, merging them all.
- M1: skip the rewrite when the managed bundle is already current, instead
  of re-harvesting and rewriting on every launch (mirrors the JetBrains
  hash-and-skip). configureNodeExtraCaCerts now returns a typed outcome
  (unchanged | written | write-failed-reused | write-failed |
  no-system-certs) with cert counts.
- M2: tolerate rename-over-existing failures (Windows EPERM/EBUSY when a
  concurrent child holds the file open) by removing the target and
  retrying, then falling back to a previously-written bundle. Combined
  with M1 the steady state no longer rewrites at all.
- M3: the wrapper prints a one-line diagnostic under CLINE_DEBUG=1
  (cert counts + managed path, or a warning when no OS certs were found
  or the write failed). Runs once per startup.
- M4: corrected the now-stale CLI guidance in shared/net.ts (the CLI no
  longer requires users to set NODE_EXTRA_CA_CERTS manually).
- L1: documented the auto-trust behavior, the managed ~/.cline bundle,
  the merge-not-replace override semantics, and CLINE_DEBUG in the CLI
  README.
- L4: trimmed the helper's file header; DI is still injectable for tests.

ca-certs.test.ts grows to 20 cases: adds readUserCerts (single path,
delimited split, missing-segment skip, managed-bundle exclusion, empty),
the unchanged/second-run skip, and a write-failure outcome via an
fs that throws.

* fix: address CLI auto-CA review issues (temp cleanup, cert count, test)

- writeBundle now hoists the temp path so the outer catch removes a
  partially-written temp file (e.g. ENOSPC / ACL failure mid-write).
  Previously only the inner double-rename failure cleaned up, so repeated
  disk-full/permission failures left a stale .tmp per launch in ~/.cline.
  The inner Windows-rename fallback now lets its failure fall through to
  the single cleanup path instead of duplicating rmSync.
- userCertCount now counts individual certificates (via countCerts, which
  tallies BEGIN CERTIFICATE markers) rather than the number of PEM files,
  so a user bundle with N intermediates reports N and is comparable to
  systemCertCount. countCerts is exported for testing.
- Adds tests for the write-failed-reused branch (stale bundle reused when
  the rewrite fails but the old file is still readable) and for countCerts
  (one file holding two certs reports 2).

* fix: warn when the CLI wrapper's Node cannot read the OS trust store

tls.getCACertificates("system") needs Node >= 22.15; on older hosts the
auto-CA harvest silently did nothing, which is indistinguishable from a
broken corporate proxy. Distinguish the missing-API case as its own
outcome (api-unavailable) and print a non-debug warning when the user
has no NODE_EXTRA_CA_CERTS of their own. Found in round-5 Windows
validation (wrapper under Node 22.1.0).

* fix: copy only certificate blocks into the managed CA bundle

Combined cert+key PEMs (nginx/haproxy-style server.pem) passed the
old contains-a-certificate check, so a user NODE_EXTRA_CA_CERTS
pointing at one duplicated the private key into the managed bundle,
where it outlives rotation of the original and gets no permission
tightening on Windows. Extract complete BEGIN/END CERTIFICATE blocks
instead; files with none are treated as not PEM, and certificates-only
files pass through byte-identical so the unchanged-skip stays stable.
Raised in PR review.

* fix: show the old-Node trust warning once per Node version

The api-unavailable warning printed on every CLI invocation, turning
an actionable nudge into stderr noise for users pinned to an old Node.
Stamp the warning per Node version under the cline dir: it shows once,
re-arms when the Node version changes, and a bookkeeping failure never
suppresses the diagnostic. Raised in PR review.
2026-07-16 10:14:15 -07:00
Dominic Cooney a41129a5db fix(vscode): restore 'Proceed While Running' for foreground terminal commands (#12320)
* First cut of 'proceed while running' for foreground tasks.

* Address review: flush partial line on detach; cap log before write; freeze partial output at detach.

* fix(vscode): cap detached command log replay
2026-07-15 22:46:37 -07:00
Saoud Rizwan 1ea34be611 chore(cli): release v3.0.42 2026-07-15 20:03:31 -07:00
143 changed files with 7712 additions and 993 deletions
+158
View File
@@ -0,0 +1,158 @@
---
name: publish-ui
description: Prepare, validate, and publish standalone @cline/ui npm releases. Use when bumping the UI package version, publishing latest or next through ui-publish.yml, checking UI release readiness, or completing the one-time npm trusted-publishing bootstrap.
---
# Publish UI
Release `@cline/ui` independently from the Cline SDK runtime packages.
## Release contract
- Version source: `sdk/packages/ui/package.json`.
- Workflow: `.github/workflows/ui-publish.yml`.
- The package keeps `internal: true` only to stay out of the SDK's shared
version/publish scripts. It is still a public npm package because
`private: false` and `publishConfig.access: public` control npm publication.
- `latest` is the production channel. `next` is an opt-in preview channel.
- Use prerelease versions such as `0.2.0-next.0` for `next`; do not publish a
version intended for `latest` under the preview tag because npm versions
cannot be republished.
- There is no UI Git tag, GitHub release, schedule, or Slack announcement.
- The workflow runs only by manual dispatch. Every release attempt runs the UI
quality checks before publishing and requires `confirm_publish=publish` from
`main`.
- The publish job and npm trust relationship use the protected `Publish`
environment.
- Every npm publication needs a new semver version; npm versions are immutable.
- Always ask before pushing commits, triggering the publish workflow, changing
npm trust settings, or running a local publish command.
## Normal release
1. Inspect the branch, current version, npm state, and UI changes.
```sh
git status --short --branch
node -p "require('./sdk/packages/ui/package.json').version"
npm view @cline/ui dist-tags versions --json
git log --oneline --no-merges -- \
sdk/packages/ui apps/examples/desktop-app/webview/components/views/chat \
.github/workflows/ui-publish.yml
```
2. Ask for the npm channel and version together. For `latest`, ask for patch,
minor, major, or an explicit version. For `next`, require an explicit
prerelease version such as `0.2.0-next.0`. Do not guess. Update only
`sdk/packages/ui/package.json` and its workspace version in `bun.lock`. Do
not run the SDK version command.
3. Validate the release candidate.
```sh
bun install --filter @cline/ui --filter @cline/code --frozen-lockfile
bun -F @cline/ui typecheck
bun -F @cline/ui test
bun -F @cline/ui test:package
bun -F @cline/ui build-storybook
bun -F @cline/code test:chat-ui
```
The packed-package test installs the tarball with Bun/React 19 and with
npm/Node/React 18.
Inspect `bun pm pack --dry-run` when the exported file set changed.
4. Commit the version bump separately from feature work. Ask before pushing.
```sh
git add sdk/packages/ui/package.json bun.lock
git commit -m "chore(ui): release vX.Y.Z"
git push origin HEAD
```
5. After the release commit reaches `main`, restate the selected npm tag and ask
for explicit publish approval. Then trigger and watch the standalone
workflow:
```sh
run_url=$(gh workflow run ui-publish.yml --ref main \
-f npm_tag=latest \
-f confirm_publish=publish)
test -n "$run_url"
run_id=${run_url##*/}
gh run watch "$run_id" --exit-status
```
Use `npm_tag=next` only for a deliberate preview. Do not report success until
the workflow succeeds and npm shows the exact version under the selected tag.
```sh
npm view @cline/ui dist-tags versions --json
```
## One-time npm bootstrap
Use this only while `npm view @cline/ui` returns `E404`. npm requires the
package to exist before its GitHub trusted publisher can be configured.
1. Merge the package and `ui-publish.yml` to `main`. Start from a clean,
reviewed `main` checkout. Verify authentication, account 2FA, and write
access to the `@cline` npm organization. The `npm trust` command in step 4
requires npm CLI 11.15 or newer; the automated trusted-publishing workflow
itself enforces npm 11.5.1 or newer.
```sh
npm --version
npm whoami
npm view @cline/ui version
```
If npm is older than 11.15, ask before upgrading with
`npm install -g npm@^11.15.0`.
2. Run the normal release validation in step 3 above. Then build, pack, test,
and inspect the exact initial tarball. Record the absolute archive path
printed by the final command.
```sh
bun -F @cline/ui build
pack_dir=$(mktemp -d)
(cd sdk/packages/ui && bun pm pack --ignore-scripts --destination "$pack_dir" --quiet)
tarball=$(find "$pack_dir" -maxdepth 1 -name '*.tgz' -print -quit)
test -n "$tarball"
bun sdk/packages/ui/scripts/smoke-package.ts "$tarball"
tar -tzf "$tarball"
printf 'Bootstrap archive: %s\n' "$tarball"
```
3. Ask for explicit approval, then publish the initial version publicly under
`latest`:
```sh
npm publish /absolute/path/from-step-2.tgz --access public --tag latest
```
4. Ask separately before configuring the standalone workflow as the trusted
publisher:
```sh
npm trust github @cline/ui \
--repo cline/cline \
--file ui-publish.yml \
--env Publish \
--allow-publish
```
5. Verify both package state and trust. Every later release uses the workflow;
do not add a long-lived npm token.
```sh
npm view @cline/ui dist-tags versions --json
npm trust list @cline/ui
```
## Final report
Report the version and npm tag, release commit, whether anything was pushed,
workflow URL or bootstrap result, npm verification, and tests/builds run. If
the package still returns `E404`, state that bootstrap remains required.
@@ -0,0 +1,4 @@
interface:
display_name: "Publish UI"
short_description: "Prepare and publish the Cline UI package"
default_prompt: "Use $publish-ui to prepare and publish a new @cline/ui npm release."
+3 -2
View File
@@ -8,8 +8,9 @@ HTTP-controlled debugger for the VSCode extension at `src/dev/debug-harness/serv
# Build extension first if needed (protos + esbuild):
bun run protos && IS_DEV=true bun esbuild.mjs
# Launch (skip-build if already built):
bun src/dev/debug-harness/server.ts --skip-build --auto-launch
# Launch (skip-build if already built). Run with node, NOT bun — Playwright's
# Electron launch times out under bun:
node src/dev/debug-harness/server.ts --skip-build --auto-launch
# In another terminal:
curl localhost:19229/api -d '{"method":"status"}'
+1
View File
@@ -16,6 +16,7 @@ This file is the secret sauce for working effectively in this codebase. It captu
- The whole repo (including `apps/vscode`) uses **bun** for package management and task running. Emit `bun run X` / `bun install` / `bunx <bin>` / `bun file.ts`, never npm/npx. Node remains the *runtime* (VS Code's extension host and the standalone cline-core are Node), so Node-runtime tokens are legitimate and must not be "fixed" to bun — see @.clinerules/bun-and-node.md for the keep-list vs rewrite-list.
- Avoid provider-specific string matching / hardcoded provider branches when fixing provider/config plumbing. Prefer provider metadata, shared catalog/defaults, explicit protocol/client capabilities, or centralized normalization utilities that apply by data shape rather than `providerId === "..."`. If a provider exception seems necessary, stop and explain why instead of adding ad-hoc string matching.
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `bun run compile`, not `bun run build`).
- When reading a configuration files that users may edit, use `readFileStrippingUtf8Bom`, `readFileSyncStrippingUtf8Bom`, or `stripUtf8Bom` from `@cline/shared/node`. DON'T strip byte order marks of user files handled by tools/passed to models.
- When creating PRs, contributors should not create changelog-entry files. Maintainers handle release versioning and changelog curation during the release process.
- When adding new feature flags, see this PR as a reference https://github.com/cline/cline/pull/7566
- Additional instructions about making requests: @.clinerules/network.md
+145
View File
@@ -0,0 +1,145 @@
name: ui-publish
on:
workflow_dispatch:
inputs:
npm_tag:
description: "npm distribution tag"
required: true
type: choice
options:
- next
- latest
default: next
confirm_publish:
description: 'Type "publish" to publish @cline/ui to npm'
required: true
type: string
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
quality:
name: UI quality and package checks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
- name: Install dependencies
run: bun install --filter @cline/ui --filter @cline/code --frozen-lockfile
- name: Typecheck UI
run: bun -F @cline/ui typecheck
- name: Test UI
run: bun -F @cline/ui test
- name: Build Storybook
run: bun -F @cline/ui build-storybook
- name: Build UI package
run: bun -F @cline/ui build
- name: Test desktop chat integration
run: bun -F @cline/code test:chat-ui
- name: Pack publish artifact
id: pack
shell: bash
run: |
set -euo pipefail
pack_dir="$RUNNER_TEMP/ui-npm-pack"
mkdir -p "$pack_dir"
cd sdk/packages/ui
bun pm pack --ignore-scripts --destination "$pack_dir" --quiet
archive=$(find "$pack_dir" -maxdepth 1 -name '*.tgz' -print -quit)
test -n "$archive"
echo "archive=$archive" >> "$GITHUB_OUTPUT"
- name: Test packed package
env:
UI_PACKAGE_ARCHIVE: ${{ steps.pack.outputs.archive }}
run: bun sdk/packages/ui/scripts/smoke-package.ts "$UI_PACKAGE_ARCHIVE"
- name: Upload publish artifact
uses: actions/upload-artifact@v4
with:
name: ui-npm-package
path: ${{ runner.temp }}/ui-npm-pack/*.tgz
if-no-files-found: error
retention-days: 7
publish:
name: Publish @cline/ui
if: >-
github.event_name == 'workflow_dispatch' &&
github.repository == 'cline/cline' &&
github.ref == 'refs/heads/main' &&
inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
needs: quality
runs-on: ubuntu-latest
environment: Publish
permissions:
contents: read
id-token: write
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Download publish artifact
uses: actions/download-artifact@v4
with:
name: ui-npm-package
path: ${{ runner.temp }}/ui-npm-pack
- name: Verify publish tooling
shell: bash
run: |
set -euo pipefail
npm_version=$(npm --version)
echo "npm ${npm_version}"
node -e 'const [major, minor, patch] = process.argv[1].split(".").map(Number); if (major < 11 || (major === 11 && (minor < 5 || (minor === 5 && patch < 1)))) { console.error("npm 11.5.1 or newer is required for trusted publishing"); process.exit(1); }' "$npm_version"
- name: Publish package
shell: bash
env:
NPM_CONFIG_PROVENANCE: "true"
NPM_TAG: ${{ inputs.npm_tag }}
run: |
set -euo pipefail
archive=$(find "$RUNNER_TEMP/ui-npm-pack" -maxdepth 1 -name '*.tgz' -print -quit)
if [ -z "$archive" ]; then
echo "UI package archive was not downloaded"
exit 1
fi
version=$(tar -xOf "$archive" package/package.json | node -e 'let input=""; process.stdin.on("data", chunk => input += chunk); process.stdin.on("end", () => process.stdout.write(JSON.parse(input).version))')
if npm view "@cline/ui@${version}" version >/dev/null 2>&1; then
echo "@cline/ui@${version} already exists; bump sdk/packages/ui/package.json before publishing"
exit 1
fi
npm publish "$archive" --tag "$NPM_TAG" --access public
echo "Published @cline/ui@${version} with npm tag '${NPM_TAG}'"
+22
View File
@@ -1,5 +1,27 @@
# Cline CLI Changelog
## 3.0.45
- Smaller install: the Claude Code and Codex providers are now optional and loaded on demand, cutting `npm i -g cline` from ~640MB to ~285MB (from SDK v0.0.65)
- Kimi K3 is now available as a ClinePass model (from SDK v0.0.65)
- Runs now retry once after refreshing expired OAuth credentials (from SDK v0.0.65)
- Team runs: the spawn tool is no longer exposed to teammates, and errored teammate runs now report as failed instead of completed (from SDK v0.0.65)
- Hub status output now includes version numbers
- Updated the bundled model catalog (from SDK v0.0.65)
## 3.0.44
- Improved max output token handling across providers (gateway routing, OpenAI vendor, and reasoning models) (from SDK v0.0.64)
- Frontmatter and configuration files that start with a UTF-8 byte order mark (e.g. saved by Windows editors) now parse correctly (from SDK v0.0.64)
## 3.0.43
- The CLI now automatically trusts your operating system's certificate store, so it works behind corporate proxies and TLS-inspecting firewalls without manually setting `NODE_EXTRA_CA_CERTS` (fixes "unable to get local issuer certificate" errors, including Windows intermediate CA stores)
## 3.0.42
- Fixed Ollama native API routing so context window and timeout settings work again
## 3.0.41
- Compaction now shows progress status in the TUI
+15
View File
@@ -346,9 +346,24 @@ Desktop-integrated approval mode is also supported via env wiring (`CLINE_TOOL_A
- `CLINE_LOG_LEVEL` - Runtime log level (`trace|debug|info|warn|error|fatal|silent`, default `info`)
- `CLINE_LOG_PATH` - Runtime log file path (default `<CLINE_DATA_DIR>/logs/cline.log`)
- `CLINE_LOG_NAME` - Logger name embedded in runtime log records
- `CLINE_DEBUG` - Set to `1`/`true` to print wrapper diagnostics (e.g. the CA bundle summary)
`--key` takes precedence over environment variables.
## Certificate trust
The CLI automatically trusts your operating system's certificate store, so it
works behind corporate TLS-inspecting proxies and with self-signed/internal
endpoints without any setup. On launch the `cline` wrapper harvests the OS trust
anchors and writes them to `~/.cline/cli-node-extra-ca-certs.pem`, then points
the runtime's `NODE_EXTRA_CA_CERTS` at that bundle. The file is regenerated when
it changes and is safe to delete (it is rebuilt on the next run).
If you set `NODE_EXTRA_CA_CERTS` yourself, your certificates are **merged** into
that bundle alongside the system store rather than replacing it. Run with
`CLINE_DEBUG=1` to see how many OS and user CAs were loaded and where the bundle
was written.
## Contributing
See [DEVELOPMENT.md](./DEVELOPMENT.md) for local development setup, monorepo structure, and TUI architecture. See [DISTRIBUTION.md](./DISTRIBUTION.md) for how the CLI is packaged and distributed.
+281
View File
@@ -0,0 +1,281 @@
// Auto-discovery of OS trust anchors for the Cline CLI.
//
// Bun does not read the OS trust store, so the 3.x CLI cannot see corporate
// MITM / self-signed CAs out of the box. This runs in the Node `bin/cline`
// wrapper (not Bun), reads the full OS store via tls.getCACertificates("system")
// (Node >= 22, no --use-system-ca flag), and hands the certs to the Bun child
// via NODE_EXTRA_CA_CERTS, which both runtimes honor. Mirrors the JetBrains
// plugin's configureCertificates(), sourcing from the OS instead of the IDE.
//
// Dependency-free CommonJS with injectable modules so it is unit-testable and
// ships verbatim in the published wrapper package.
const PEM_MARKER = "-----BEGIN CERTIFICATE-----";
const CERT_BLOCK =
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
/**
* Returns only the complete certificate blocks from PEM text, or null when
* there are none. User files may also hold private keys (combined cert+key
* PEMs) or other sections, which must never be copied into the managed
* bundle. Files that contain nothing but certificates pass through verbatim
* so unchanged bundles keep hash-skipping the rewrite.
*/
function sanitizePem(text) {
const blocks = text.match(CERT_BLOCK) ?? [];
if (blocks.length === 0) {
return null;
}
const rest = text.replace(CERT_BLOCK, "");
if (/^\s*$/.test(rest)) {
return text;
}
return `${blocks.join("\n")}\n`;
}
/**
* Returns OS-trusted certificates as PEM strings, or [] when unavailable.
* tls.getCACertificates("system") requires Node >= 22.
*/
function harvestSystemCerts(tlsModule) {
try {
const tls = tlsModule || require("node:tls");
if (typeof tls.getCACertificates !== "function") {
return [];
}
const certs = tls.getCACertificates("system");
if (!Array.isArray(certs)) {
return [];
}
return certs.filter(
(cert) => typeof cert === "string" && cert.includes(PEM_MARKER),
);
} catch {
return [];
}
}
/**
* Returns the file's certificate blocks as PEM text, or null when missing,
* unreadable, or holding no complete certificate block.
*/
function readUserBundle(fsModule, userPath) {
if (!userPath) {
return null;
}
try {
const fs = fsModule || require("node:fs");
const stat = fs.statSync(userPath, { throwIfNoEntry: false });
if (!stat || !stat.isFile()) {
return null;
}
// Binary DER would not have loaded in the runtime either; require PEM.
return sanitizePem(fs.readFileSync(userPath, "utf8"));
} catch {
return null;
}
}
/**
* Reads the user's NODE_EXTRA_CA_CERTS value into PEM strings. Node treats the
* value as a single file, but some users set an OS-path-delimited list; the
* whole value is tried as one file first, then split.
* The managed bundle is excluded so reading it back never re-appends its certs.
*/
function readUserCerts(fsModule, pathModule, value, managedPath) {
if (!value) {
return [];
}
const fs = fsModule || require("node:fs");
const path = pathModule || require("node:path");
const candidates = [];
const whole = readUserBundle(fs, value);
if (whole) {
candidates.push({ filePath: value, pem: whole });
} else if (value.includes(path.delimiter)) {
for (const segment of value.split(path.delimiter)) {
const trimmed = segment.trim();
if (!trimmed) {
continue;
}
const pem = readUserBundle(fs, trimmed);
if (pem) {
candidates.push({ filePath: trimmed, pem });
}
}
}
const pems = [];
for (const candidate of candidates) {
const isManaged =
managedPath &&
path.resolve(candidate.filePath) === path.resolve(managedPath);
if (!isManaged) {
pems.push(candidate.pem);
}
}
return pems;
}
/**
* Concatenates the user PEMs (if any) and the system certificates into one
* bundle. A separating newline is inserted between parts so adjacent END/BEGIN
* markers cannot fuse into one invalid line.
*/
function buildBundle({ systemCerts, userPems }) {
const parts = [...(userPems ?? []), ...systemCerts];
return parts
.map((part) => (part.endsWith("\n") ? part : `${part}\n`))
.join("");
}
/** Counts individual PEM certificates across the given bundle strings. */
function countCerts(pems) {
let count = 0;
for (const pem of pems) {
count += pem.split(PEM_MARKER).length - 1;
}
return count;
}
function readFileIfExists(fs, filePath) {
try {
return fs.readFileSync(filePath, "utf8");
} catch {
return null;
}
}
function resolveClineDir(env, os, path) {
return env.CLINE_DIR?.trim() || path.join(os.homedir(), ".cline");
}
/**
* True when the api-unavailable warning should print. Stamped per Node version
* in the cline dir so the nudge shows once rather than on every command; a
* version change (upgrade that still falls short, or downgrade) re-arms it.
* When the stamp cannot be read or written, warn — bookkeeping failures must
* never suppress a real diagnostic.
*/
function shouldWarnApiUnavailable(env, deps = {}) {
const fs = deps.fs || require("node:fs");
const os = deps.os || require("node:os");
const path = deps.path || require("node:path");
const version = deps.nodeVersion || process.versions.node;
const dir = resolveClineDir(env, os, path);
const stamp = path.join(dir, `.ca-api-warned-${version}`);
try {
if (fs.existsSync(stamp)) {
return false;
}
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(stamp, "", { mode: 0o600 });
return true;
} catch {
return true;
}
}
/** Atomically writes [content] to [target]; returns true on success. */
function writeBundle(fs, dir, target, content) {
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
try {
fs.mkdirSync(dir, { recursive: true });
// Owner read/write: the bundle holds public CA material, not secrets,
// but there is no reason to make it world-writable.
fs.writeFileSync(tmp, content, { mode: 0o600 });
try {
fs.renameSync(tmp, target);
} catch {
// Windows can reject rename over a file a concurrent child holds open.
fs.rmSync(target, { force: true });
fs.renameSync(tmp, target);
}
return true;
} catch {
// Never leave a partial temp file behind (e.g. ENOSPC mid-write).
try {
fs.rmSync(tmp, { force: true });
} catch {
// Ignore: best-effort cleanup.
}
return false;
}
}
/**
* Harvests OS trust anchors, merges them with any user NODE_EXTRA_CA_CERTS, and
* points env.NODE_EXTRA_CA_CERTS at a single managed PEM bundle. Mutates `env`
* in place. Returns an outcome the caller can log; `action` is one of
* "unchanged" | "written" | "write-failed-reused" | "write-failed" |
* "no-system-certs" | "api-unavailable".
*/
function configureNodeExtraCaCerts(env, deps = {}) {
const fs = deps.fs || require("node:fs");
const os = deps.os || require("node:os");
const path = deps.path || require("node:path");
const tls = deps.tls || require("node:tls");
// tls.getCACertificates("system") needs Node >= 22.15; on older Nodes the
// harvest cannot run at all, which the caller should surface to the user.
if (typeof tls.getCACertificates !== "function") {
return {
action: "api-unavailable",
path: null,
systemCertCount: 0,
userCertCount: 0,
};
}
const systemCerts = harvestSystemCerts(tls);
if (systemCerts.length === 0) {
// Nothing to add: leave any user-provided NODE_EXTRA_CA_CERTS untouched
// and let the runtime fall back to its bundled CAs.
return {
action: "no-system-certs",
path: null,
systemCertCount: 0,
userCertCount: 0,
};
}
const managedDir = resolveClineDir(env, os, path);
const managedPath = path.join(managedDir, "cli-node-extra-ca-certs.pem");
const userValue = (env.NODE_EXTRA_CA_CERTS || "").trim() || null;
const userPems = readUserCerts(fs, path, userValue, managedPath);
const bundle = buildBundle({ systemCerts, userPems });
const base = {
path: managedPath,
systemCertCount: systemCerts.length,
userCertCount: countCerts(userPems),
};
// Skip the rewrite when the bundle is already current. Avoids per-launch I/O
// and the concurrent-rename race in the steady state.
if (readFileIfExists(fs, managedPath) === bundle) {
env.NODE_EXTRA_CA_CERTS = managedPath;
return { ...base, action: "unchanged" };
}
if (writeBundle(fs, managedDir, managedPath, bundle)) {
env.NODE_EXTRA_CA_CERTS = managedPath;
return { ...base, action: "written" };
}
// Write failed: fall back to a previously-written bundle if one exists.
if (readFileIfExists(fs, managedPath)) {
env.NODE_EXTRA_CA_CERTS = managedPath;
return { ...base, action: "write-failed-reused" };
}
return { ...base, path: null, action: "write-failed" };
}
module.exports = {
harvestSystemCerts,
sanitizePem,
readUserBundle,
readUserCerts,
buildBundle,
countCerts,
configureNodeExtraCaCerts,
shouldWarnApiUnavailable,
};
+42
View File
@@ -23,6 +23,48 @@ const childEnv = {
CLINE_WRAPPER_PATH: scriptPath,
};
// Auto-discover OS trust anchors and pass them to the Bun child via
// NODE_EXTRA_CA_CERTS. The Bun runtime does not read the OS store on its own,
// so corporate/self-signed CAs would otherwise fail. This wrapper runs on
// Node, which can read the full store here.
try {
const caCerts = require("./ca-certs.cjs");
const outcome = caCerts.configureNodeExtraCaCerts(childEnv);
const debug =
process.env.CLINE_DEBUG === "1" || process.env.CLINE_DEBUG === "true";
// Not debug-gated: on old Nodes the harvest silently doing nothing is
// indistinguishable from a broken corporate proxy. Stamped per Node
// version so the nudge shows once, not on every command.
if (
outcome &&
outcome.action === "api-unavailable" &&
!childEnv.NODE_EXTRA_CA_CERTS &&
caCerts.shouldWarnApiUnavailable(childEnv)
) {
console.warn(
`[cline] Node ${process.versions.node} cannot read the OS trust store (needs >= 22.15); ` +
"corporate or self-signed CAs may fail TLS. Upgrade Node or set NODE_EXTRA_CA_CERTS.",
);
}
if (debug && outcome) {
if (outcome.action === "no-system-certs") {
console.warn(
"[cline] No OS trust anchors found; relying on the runtime's bundled CAs.",
);
} else if (outcome.action === "write-failed") {
console.warn(
"[cline] Could not write the managed CA bundle; relying on the runtime's bundled CAs.",
);
} else {
console.warn(
`[cline] Trust: ${outcome.systemCertCount} OS + ${outcome.userCertCount} user CAs (${outcome.action}) -> ${outcome.path}`,
);
}
}
} catch {
// Best effort: fall back to the runtime's default trust on any failure.
}
function run(target) {
const result = childProcess.spawnSync(target, process.argv.slice(2), {
stdio: "inherit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cline/cli",
"displayName": "cline",
"version": "3.0.41",
"version": "3.0.45",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"type": "module",
"publishConfig": {
+367
View File
@@ -0,0 +1,367 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
// The helper ships as CommonJS in the published wrapper package, so it is
// loaded via require rather than an ESM import.
const caCerts = require("../../bin/ca-certs.cjs") as {
harvestSystemCerts: (tls?: unknown) => string[];
readUserBundle: (fs: unknown, p: string | null) => string | null;
readUserCerts: (
fs: unknown,
path: unknown,
value: string | null,
managedPath: string | null,
) => string[];
buildBundle: (input: {
systemCerts: string[];
userPems?: string[];
}) => string;
countCerts: (pems: string[]) => number;
configureNodeExtraCaCerts: (
env: Record<string, string>,
deps?: { tls?: unknown; fs?: unknown },
) => {
action: string;
path: string | null;
systemCertCount: number;
userCertCount: number;
};
shouldWarnApiUnavailable: (
env: Record<string, string>,
deps?: { fs?: unknown; nodeVersion?: string },
) => boolean;
};
const fs = require("node:fs");
const path = require("node:path");
const certSystem =
"-----BEGIN CERTIFICATE-----\nSYSTEM\n-----END CERTIFICATE-----\n";
const certUser = "-----BEGIN CERTIFICATE-----\nUSER\n-----END CERTIFICATE-----";
function fakeTls(certs: unknown) {
return { getCACertificates: () => certs };
}
describe("ca-certs", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "cline-ca-"));
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
describe("harvestSystemCerts", () => {
it("returns only PEM strings from the system store", () => {
expect(
caCerts.harvestSystemCerts(fakeTls([certSystem, "not-a-cert", 42])),
).toEqual([certSystem]);
});
it("returns [] when getCACertificates is unavailable", () => {
expect(caCerts.harvestSystemCerts({})).toEqual([]);
});
it("returns [] when getCACertificates throws", () => {
expect(
caCerts.harvestSystemCerts({
getCACertificates: () => {
throw new Error("nope");
},
}),
).toEqual([]);
});
});
describe("readUserBundle", () => {
it("returns PEM contents for a PEM file", () => {
const p = join(dir, "user.pem");
writeFileSync(p, certUser);
expect(caCerts.readUserBundle(fs, p)).toBe(certUser);
});
it("returns null for a non-PEM (DER) file", () => {
const p = join(dir, "user.der");
writeFileSync(p, Buffer.from([0x30, 0x82, 0x01, 0x02]));
expect(caCerts.readUserBundle(fs, p)).toBeNull();
});
it("returns null for a missing file and for null path", () => {
expect(caCerts.readUserBundle(fs, join(dir, "nope.pem"))).toBeNull();
expect(caCerts.readUserBundle(fs, null)).toBeNull();
});
it("strips non-certificate sections such as private keys", () => {
// Combined cert+key files (nginx/haproxy style) are common; the key
// must never reach the managed bundle.
const p = join(dir, "combined.pem");
writeFileSync(
p,
`${certUser}\n-----BEGIN PRIVATE KEY-----\nSECRET\n-----END PRIVATE KEY-----\n`,
);
const out = caCerts.readUserBundle(fs, p);
expect(out).toContain("USER");
expect(out).not.toContain("PRIVATE KEY");
expect(out).not.toContain("SECRET");
});
it("keeps certificates-only files verbatim", () => {
// Byte-identical passthrough keeps the unchanged-skip hash stable.
const p = join(dir, "clean.pem");
writeFileSync(p, `${certUser}\n${certSystem}`);
expect(caCerts.readUserBundle(fs, p)).toBe(`${certUser}\n${certSystem}`);
});
it("returns null for a BEGIN marker without a complete block", () => {
const p = join(dir, "truncated.pem");
writeFileSync(p, "-----BEGIN CERTIFICATE-----\ntruncated");
expect(caCerts.readUserBundle(fs, p)).toBeNull();
});
});
describe("readUserCerts", () => {
it("reads a single PEM file path", () => {
const p = join(dir, "corp.pem");
writeFileSync(p, certUser);
expect(caCerts.readUserCerts(fs, path, p, null)).toEqual([certUser]);
});
it("splits a legacy OS-path-delimited value and reads each PEM", () => {
// Legacy footgun: NODE_EXTRA_CA_CERTS="a.pem;b.pem".
const a = join(dir, "a.pem");
const b = join(dir, "b.pem");
writeFileSync(a, certUser);
writeFileSync(b, certSystem);
expect(
caCerts.readUserCerts(fs, path, [a, b].join(delimiter), null),
).toEqual([certUser, certSystem]);
});
it("skips missing segments in a delimited value", () => {
const a = join(dir, "a.pem");
writeFileSync(a, certUser);
const value = [a, join(dir, "missing.pem")].join(delimiter);
expect(caCerts.readUserCerts(fs, path, value, null)).toEqual([certUser]);
});
it("excludes the managed bundle from user certs", () => {
const managed = join(dir, "cli-node-extra-ca-certs.pem");
writeFileSync(managed, certUser);
expect(caCerts.readUserCerts(fs, path, managed, managed)).toEqual([]);
});
it("returns [] for empty value", () => {
expect(caCerts.readUserCerts(fs, path, null, null)).toEqual([]);
});
});
describe("buildBundle", () => {
it("merges user PEMs before system certs", () => {
expect(
caCerts.buildBundle({
systemCerts: [certSystem],
userPems: [certUser],
}),
).toBe(`${certUser}\n${certSystem}`);
});
it("inserts a separating newline so END/BEGIN markers do not fuse", () => {
// certUser has no trailing newline, so this proves the boundary fix.
const merged = caCerts.buildBundle({
systemCerts: [certSystem],
userPems: [certUser],
});
expect(merged).not.toContain(
"-----END CERTIFICATE----------BEGIN CERTIFICATE-----",
);
});
it("handles no user PEMs", () => {
expect(caCerts.buildBundle({ systemCerts: [certSystem] })).toBe(
certSystem,
);
});
});
describe("configureNodeExtraCaCerts", () => {
it("writes a managed bundle and points the env var at it", () => {
const env: Record<string, string> = { CLINE_DIR: dir };
const out = caCerts.configureNodeExtraCaCerts(env, {
tls: fakeTls([certSystem]),
});
expect(out.action).toBe("written");
expect(out.path).toBe(join(dir, "cli-node-extra-ca-certs.pem"));
expect(env.NODE_EXTRA_CA_CERTS).toBe(out.path);
expect(readFileSync(out.path as string, "utf8")).toContain("SYSTEM");
});
it("merges a user-supplied NODE_EXTRA_CA_CERTS with system certs", () => {
const userPath = join(dir, "corp.pem");
writeFileSync(userPath, certUser);
const env: Record<string, string> = {
CLINE_DIR: dir,
NODE_EXTRA_CA_CERTS: userPath,
};
const out = caCerts.configureNodeExtraCaCerts(env, {
tls: fakeTls([certSystem]),
});
expect(out.userCertCount).toBe(1);
const written = readFileSync(env.NODE_EXTRA_CA_CERTS, "utf8");
expect(written).toContain("USER");
expect(written).toContain("SYSTEM");
});
it("reports unchanged and skips rewrite on the second run", () => {
const env: Record<string, string> = { CLINE_DIR: dir };
expect(
caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([certSystem]) })
.action,
).toBe("written");
expect(
caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([certSystem]) })
.action,
).toBe("unchanged");
});
it("does not re-append when the user already points at the managed bundle", () => {
const env: Record<string, string> = { CLINE_DIR: dir };
const first = caCerts.configureNodeExtraCaCerts(env, {
tls: fakeTls([certSystem]),
}).path as string;
const env2: Record<string, string> = {
CLINE_DIR: dir,
NODE_EXTRA_CA_CERTS: first,
};
caCerts.configureNodeExtraCaCerts(env2, { tls: fakeTls([certSystem]) });
const written = readFileSync(env2.NODE_EXTRA_CA_CERTS, "utf8");
expect(written.match(/SYSTEM/g)?.length).toBe(1);
});
it("no-ops when no system certs are available", () => {
const env: Record<string, string> = {
CLINE_DIR: dir,
NODE_EXTRA_CA_CERTS: "/user/corp.pem",
};
const out = caCerts.configureNodeExtraCaCerts(env, { tls: fakeTls([]) });
expect(out.action).toBe("no-system-certs");
expect(out.path).toBeNull();
expect(env.NODE_EXTRA_CA_CERTS).toBe("/user/corp.pem");
});
it("reports api-unavailable on Nodes without getCACertificates", () => {
const env: Record<string, string> = {
CLINE_DIR: dir,
NODE_EXTRA_CA_CERTS: "/user/corp.pem",
};
const out = caCerts.configureNodeExtraCaCerts(env, { tls: {} });
expect(out.action).toBe("api-unavailable");
expect(out.path).toBeNull();
expect(env.NODE_EXTRA_CA_CERTS).toBe("/user/corp.pem");
});
it("reports write-failed when the bundle cannot be written", () => {
const realFs = require("node:fs");
const failingFs = {
...realFs,
mkdirSync: () => {
throw new Error("EACCES");
},
writeFileSync: () => {
throw new Error("EACCES");
},
};
const env: Record<string, string> = { CLINE_DIR: dir };
const out = caCerts.configureNodeExtraCaCerts(env, {
tls: fakeTls([certSystem]),
fs: failingFs,
});
expect(out.action).toBe("write-failed");
expect(out.path).toBeNull();
expect(env.NODE_EXTRA_CA_CERTS).toBeUndefined();
});
it("reuses a stale bundle when the rewrite fails", () => {
// First run writes the bundle normally.
const env: Record<string, string> = { CLINE_DIR: dir };
const managedPath = caCerts.configureNodeExtraCaCerts(env, {
tls: fakeTls([certSystem]),
}).path as string;
// Second run: writes fail, but the stale bundle is still readable.
const realFs = require("node:fs");
const failingFs = {
...realFs,
mkdirSync: () => {
throw new Error("EACCES");
},
writeFileSync: () => {
throw new Error("EACCES");
},
};
const env2: Record<string, string> = { CLINE_DIR: dir };
const out = caCerts.configureNodeExtraCaCerts(env2, {
// A different system cert forces a rewrite attempt (not "unchanged").
tls: fakeTls([certUser]),
fs: failingFs,
});
expect(out.action).toBe("write-failed-reused");
expect(env2.NODE_EXTRA_CA_CERTS).toBe(managedPath);
});
});
describe("countCerts", () => {
it("counts individual certificates, not files", () => {
// One file holding two certs must report 2, not 1.
const twoInOne = `${certUser}\n${certSystem}`;
expect(caCerts.countCerts([twoInOne])).toBe(2);
expect(caCerts.countCerts([certUser, certSystem])).toBe(2);
expect(caCerts.countCerts([])).toBe(0);
});
});
describe("shouldWarnApiUnavailable", () => {
it("warns once per Node version, then stays quiet", () => {
const env = { CLINE_DIR: dir };
const deps = { nodeVersion: "22.1.0" };
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(false);
});
it("re-arms when the Node version changes", () => {
const env = { CLINE_DIR: dir };
expect(
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.1.0" }),
).toBe(true);
expect(
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.14.0" }),
).toBe(true);
expect(
caCerts.shouldWarnApiUnavailable(env, { nodeVersion: "22.1.0" }),
).toBe(false);
});
it("still warns when the stamp cannot be written", () => {
const realFs = require("node:fs");
const failingFs = {
...realFs,
mkdirSync: () => {
throw new Error("EACCES");
},
writeFileSync: () => {
throw new Error("EACCES");
},
};
const env = { CLINE_DIR: dir };
const deps = { fs: failingFs, nodeVersion: "22.1.0" };
// Bookkeeping failure must never suppress the diagnostic.
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
expect(caCerts.shouldWarnApiUnavailable(env, deps)).toBe(true);
});
});
});
+3 -2
View File
@@ -1,4 +1,4 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { existsSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { basename, extname, join } from "node:path";
import {
@@ -15,6 +15,7 @@ import {
type SkillConfig,
type WorkflowConfig,
} from "@cline/core";
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
import { Command } from "commander";
import { getToolCatalog } from "../runtime/tools";
import { loadInteractiveConfigData } from "../tui/interactive-config";
@@ -209,7 +210,7 @@ async function runAgentsConfigCommand(
continue;
}
const filePath = join(directory, entry.name);
const raw = readFileSync(filePath, "utf8");
const raw = readFileSyncStrippingUtf8Bom(filePath);
const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const frontmatter = frontmatterMatch?.[1] ?? "";
const nameMatch = frontmatter.match(/^\s*name:\s*(.+?)\s*$/m);
+35
View File
@@ -9,6 +9,7 @@ import {
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { version as cliVersion } from "../../package.json";
import { getCliBuildInfo } from "../utils/common";
const {
@@ -174,6 +175,40 @@ describe("runDoctorCommand", () => {
);
});
it("reports CLI and running hub Core versions", async () => {
const cwd = "/workspace";
mockReadHubDiscovery.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
port: 25463,
pid: 50174,
coreVersion: "0.0.63",
});
mockProbeHubServer.mockResolvedValue({
url: "ws://127.0.0.1:25463/hub",
port: 25463,
pid: 50174,
coreVersion: "0.0.64",
});
mockSpawnSync.mockReturnValue({ status: 1, stdout: "" });
const output: string[] = [];
const code = await runDoctorCommand(
{ cwd, json: true },
{
writeln: (text) => {
output.push(text ?? "");
},
writeErr: () => {},
},
);
expect(code).toBe(0);
expect(JSON.parse(output[0] || "")).toMatchObject({
cliVersion,
coreVersion: "0.0.64",
});
});
it("doctor --fix clears wedged hub startup artifacts when no server is actually running", async () => {
const cwd = mkdtempSync(path.join(os.tmpdir(), "doctor-hub-fix-"));
tempDirs.push(cwd);
+7
View File
@@ -14,6 +14,7 @@ import {
import { formatUptime, resolveClineBuildEnv } from "@cline/shared";
import { Command } from "commander";
import open from "open";
import { version as cliVersion } from "../../package.json";
import { isProcessRunning } from "../connectors/common";
import {
type ActiveConnectorRecord,
@@ -49,6 +50,8 @@ type SpawnedProcessRecord = {
type DoctorStatus = {
cwd: string;
cliVersion: string;
coreVersion?: string;
hubUrl?: string;
hubHealthy: boolean;
hubPid?: number;
@@ -337,6 +340,8 @@ async function collectDoctorStatus(cwd: string): Promise<DoctorStatus> {
];
return {
cwd,
cliVersion,
coreVersion: health?.coreVersion ?? discovery?.coreVersion,
hubUrl: current?.url,
hubHealthy: !!health?.url,
hubPid: current?.pid,
@@ -419,6 +424,8 @@ export async function runDoctorCommand(
io.writeln(JSON.stringify(before));
return 0;
}
writeln(`cli version ${c.dim}${before.cliVersion}${c.reset}`);
writeln(`core version ${c.dim}${before.coreVersion ?? "n/a"}${c.reset}`);
writeln(`hub url ${c.dim}${before.hubUrl ?? "none"}${c.reset}`);
writeln(
`hub healthy ${c.dim}${before.hubHealthy ? "yes" : "no"}${before.hubPid ? ` (pid=${before.hubPid})` : ""}${c.reset}`,
+4
View File
@@ -34,6 +34,7 @@ vi.mock("@cline/core", () => ({
stopLocalHubServerGracefully: mockStopLocalHubServerGracefully,
}));
import { version as cliVersion } from "../../package.json";
import { createHubCommand } from "./hub";
const originalBuildEnv = process.env.CLINE_BUILD_ENV;
@@ -63,6 +64,7 @@ describe("createHubCommand", () => {
port: 25463,
pid: 50174,
startedAt: "2026-01-01T00:00:00.000Z",
coreVersion: "0.0.62",
});
const output: string[] = [];
@@ -88,6 +90,8 @@ describe("createHubCommand", () => {
pid: 50174,
startedAt: "2026-01-01T00:00:00.000Z",
uptime: "1m 5s",
cliVersion,
coreVersion: "0.0.62",
});
});
+3
View File
@@ -9,6 +9,7 @@ import {
} from "@cline/core";
import { formatUptime, resolveClineBuildEnv } from "@cline/shared";
import { Command } from "commander";
import { version as cliVersion } from "../../package.json";
interface HubCommandIo {
writeln: (text?: string) => void;
@@ -134,6 +135,8 @@ export function createHubCommand(
pid: health?.pid,
startedAt: health?.startedAt,
uptime,
cliVersion,
coreVersion: health?.coreVersion ?? discovery?.coreVersion,
}),
);
}),
+2 -1
View File
@@ -27,6 +27,7 @@ import {
type UserInstructionConfigService,
type WorkflowConfig,
} from "@cline/core";
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
import { getToolCatalog } from "../runtime/tools";
import {
type InteractiveSlashCommand,
@@ -195,7 +196,7 @@ function loadAgentConfigItems(workspaceRoot: string): InteractiveConfigItem[] {
continue;
}
const filePath = join(directory, entry.name);
const raw = readFileSync(filePath, "utf8");
const raw = readFileSyncStrippingUtf8Bom(filePath);
const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const frontmatter = frontmatterMatch?.[1] ?? "";
const nameMatch = frontmatter.match(/^\s*name:\s*(.+?)\s*$/m);
@@ -18,6 +18,7 @@ import {
resolvePluginConfigSearchPaths,
resolveAgentConfigSearchPaths as resolveSharedAgentConfigSearchPaths,
} from "@cline/core";
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
import { readMcpServersResponse } from "./mcp";
import type { JsonRecord } from "./types";
@@ -114,7 +115,7 @@ export async function listUserInstructionConfigs(
const ext = extname(entry.name).toLowerCase();
if (ext !== ".yml" && ext !== ".yaml") continue;
const filePath = join(directory, entry.name);
const raw = readFileSync(filePath, "utf8");
const raw = readFileSyncStrippingUtf8Bom(filePath);
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const fm = fmMatch?.[1] ?? "";
const nameMatch = fm.match(/^\s*name:\s*(.+?)\s*$/m);
+6
View File
@@ -3,9 +3,12 @@
"version": "0.0.1",
"private": true,
"scripts": {
"build:ui": "bun -F @cline/ui build",
"predev:web": "bun run build:ui",
"dev:web": "next dev webview -p 3125 --turbo",
"dev:sidecar": "bun run sidecar/index.ts",
"dev": "tauri dev",
"prebuild": "bun run build:ui",
"build": "bun run bun.mts",
"build:sidecar": "mkdir -p dist/sidecar && bun build ./sidecar/index.ts --outfile ./dist/sidecar/index.js --target bun",
"build:sidecar:bin": "bun run scripts/build-sidecar-bin.ts",
@@ -16,7 +19,10 @@
"package:desktop:windows": "bun run scripts/package-desktop.ts --platform windows",
"package:desktop:linux": "bun run scripts/package-desktop.ts --platform linux",
"start": "next start webview",
"pretypecheck": "bun run build:ui",
"typecheck": "tsc -p tsconfig.dev.json --noEmit",
"pretest:chat-ui": "bun run build:ui",
"test:chat-ui": "vitest run webview/components/views/chat/chat-messages.test.tsx --config vitest.config.ts",
"clean": "rm -rf webview/.next webview/out node_modules dist && (cd src-tauri && rm -rf target node_modules dist)"
},
"dependencies": {
@@ -50,6 +50,7 @@ import {
updateMcpSettingsFileSync,
} from "@cline/core";
import { getClineEnvironmentConfig } from "@cline/shared";
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
import {
connectorChannelsPayload,
startConnectorChannel,
@@ -558,7 +559,7 @@ async function listUserInstructionConfigs(
const ext = extname(entry.name).toLowerCase();
if (ext !== ".yml" && ext !== ".yaml") continue;
const filePath = join(directory, entry.name);
const raw = readFileSync(filePath, "utf8");
const raw = readFileSyncStrippingUtf8Bom(filePath);
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const fm = fmMatch?.[1] ?? "";
const nameMatch = fm.match(/^\s*name:\s*(.+?)\s*$/m);
@@ -3,6 +3,7 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "@cline/ui/theme/index.css";
@import "@cline/ui/components/agent-chat.css";
@source "../../node_modules/streamdown/dist";
@@ -1,12 +1,27 @@
"use client";
import {
Message as AgentMessage,
Conversation,
ConversationContent,
ConversationScrollButton,
ConversationViewport,
MessageAction,
MessageActions,
MessageContent,
Reasoning,
ReasoningContent,
ReasoningTrigger,
ToolActivity,
ToolActivityCode,
ToolActivityContent,
ToolActivityDetails,
ToolActivityTrigger,
} from "@cline/ui/components/agent-chat";
import {
AlertCircle,
Bot,
BrainIcon,
Check,
ChevronDown,
ChevronRight,
Clock3,
Copy,
FileEdit,
@@ -20,15 +35,7 @@ import {
SquareTerminalIcon,
UndoIcon,
} from "lucide-react";
import {
memo,
useCallback,
useEffect,
useId,
useLayoutEffect,
useRef,
useState,
} from "react";
import { memo, useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { toast } from "@/hooks/use-toast";
import type { ChatMessage, ChatSessionStatus } from "@/lib/chat-schema";
@@ -86,11 +93,9 @@ type AskQuestionRequestItem = {
};
const IS_DEBUG = process.env.NODE_ENV === "test";
const STICKY_BOTTOM_THRESHOLD_PX = 24;
const SCROLL_TO_BOTTOM_BUTTON_THRESHOLD_PX = 120;
function ChatMessagesImpl({
sessionId: _sessionId,
sessionId,
status,
chatTransportState = "connecting",
isSessionSwitching = false,
@@ -105,9 +110,6 @@ function ChatMessagesImpl({
onRestoreCheckpoint,
onForkSession,
}: ChatMessagesProps) {
const scrollAreaRef = useRef<HTMLDivElement | null>(null);
const scrollContentRef = useRef<HTMLDivElement | null>(null);
const shouldStickToBottomRef = useRef(true);
const hasMessages = messages.length > 0;
const lastErrorMessage = [...messages]
.reverse()
@@ -115,7 +117,6 @@ function ChatMessagesImpl({
const shouldShowErrorBanner =
Boolean(error) && (!lastErrorMessage || lastErrorMessage.content !== error);
const [showSwitchTransition, setShowSwitchTransition] = useState(false);
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
const [toolApprovalActions, setToolApprovalActions] = useState<
Record<string, "approving" | "rejecting">
>({});
@@ -140,23 +141,6 @@ function ChatMessagesImpl({
const showIdleDetails =
!hasMessages && !isSessionSwitching && !showSwitchTransition;
const getViewport = useCallback(() => {
return scrollAreaRef.current;
}, []);
const scrollToBottom = useCallback(
(behavior: ScrollBehavior = "smooth") => {
const viewport = getViewport();
if (!viewport) {
return;
}
shouldStickToBottomRef.current = true;
viewport.scrollTo({ top: viewport.scrollHeight, behavior });
setShowScrollToBottom((prev) => (prev ? false : prev));
},
[getViewport],
);
useEffect(() => {
if (!isSessionSwitching) {
setShowSwitchTransition((prev) => (prev ? false : prev));
@@ -170,50 +154,6 @@ function ChatMessagesImpl({
};
}, [isSessionSwitching]);
useEffect(() => {
const viewport = getViewport();
if (!viewport) {
return;
}
const updateScrollToBottomVisibility = () => {
const distanceFromBottom =
viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
shouldStickToBottomRef.current =
distanceFromBottom <= STICKY_BOTTOM_THRESHOLD_PX;
const shouldShow =
distanceFromBottom > SCROLL_TO_BOTTOM_BUTTON_THRESHOLD_PX;
setShowScrollToBottom((prev) =>
prev === shouldShow ? prev : shouldShow,
);
};
updateScrollToBottomVisibility();
viewport.addEventListener("scroll", updateScrollToBottomVisibility);
return () => {
viewport.removeEventListener("scroll", updateScrollToBottomVisibility);
};
}, [getViewport]);
useLayoutEffect(() => {
if (!shouldStickToBottomRef.current) {
return;
}
scrollToBottom("auto");
}, [scrollToBottom]);
useEffect(() => {
const content = scrollContentRef.current;
if (!content || typeof ResizeObserver === "undefined") return;
const resizeObserver = new ResizeObserver(() => {
if (shouldStickToBottomRef.current) scrollToBottom("auto");
});
resizeObserver.observe(content);
return () => resizeObserver.disconnect();
}, [scrollToBottom]);
useEffect(() => {
const activeRequestIds = new Set(
pendingToolApprovals.map((item) => item.requestId),
@@ -377,17 +317,19 @@ function ChatMessagesImpl({
);
return (
<div className="relative h-full min-h-0 min-w-0">
<div
className="h-full min-h-0 min-w-0 overflow-x-hidden overflow-y-auto"
ref={scrollAreaRef}
<Conversation
className="h-full min-h-0 min-w-0"
key={sessionId ?? "new-chat"}
>
<ConversationViewport
aria-label="Agent conversation"
className="h-full min-h-0 min-w-0"
>
<div
<ConversationContent
className={cn(
"relative mx-auto min-h-full w-full min-w-0 max-w-full overflow-x-hidden",
showIdleDetails ? "p-0" : "px-6 py-6",
)}
ref={scrollContentRef}
>
{showIdleDetails ? null : (
<div className="flex min-h-full w-full min-w-0 flex-col gap-2 overflow-x-hidden">
@@ -497,21 +439,10 @@ function ChatMessagesImpl({
{error}
</div>
) : null}
</div>
</div>
{showScrollToBottom ? (
<Button
className="absolute bottom-4 right-4 z-20 size-9 rounded-full shadow-sm"
onClick={() => scrollToBottom("smooth")}
size="icon"
type="button"
variant="secondary"
>
<ChevronDown className="size-4" />
<span className="sr-only">Scroll to bottom</span>
</Button>
) : null}
</div>
</ConversationContent>
</ConversationViewport>
<ConversationScrollButton />
</Conversation>
);
}
@@ -746,8 +677,6 @@ function MessageBubble({
isUser && Boolean(onCopyRawText || checkpoint);
const keepUserActionsVisible = restorePending || Boolean(restoreError);
const keepAssistantActionsVisible = forkPending || Boolean(forkError);
const hiddenActionButtonsClassName =
"pointer-events-none opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100";
if (message.role === "tool") {
return <ToolMessageBlock message={message} />;
@@ -760,159 +689,102 @@ function MessageBubble({
const reasoningContent = message.reasoning?.trim() || "";
return (
<div
className={cn(
"flex min-w-0",
isUser ? "justify-end" : "w-full justify-start",
)}
>
<div
className={cn(
"group max-w-full min-w-0 wrap-break-word text-sm",
isUser && "flex max-w-[85%] flex-col items-end gap-1 md:max-w-[50%]",
!isUser && "flex flex-col items-start gap-2 overflow-hidden",
!isUser && !isError && "text-foreground",
isError &&
"bg-destructive/10 border border-destructive/40 text-destructive",
)}
>
<div
className={cn(
"max-w-full min-w-0 space-y-2 overflow-hidden wrap-break-word",
isUser && "rounded-sm bg-card p-2 text-foreground/80",
)}
>
{reasoningContent || message.reasoningRedacted ? (
<ReasoningBlock
content={reasoningContent}
redacted={message.reasoningRedacted === true}
streaming={isStreaming}
/>
) : null}
<AgentMessage from={message.role}>
<MessageContent className="space-y-2 wrap-break-word">
{reasoningContent || message.reasoningRedacted ? (
<ReasoningBlock
content={reasoningContent}
redacted={message.reasoningRedacted === true}
streaming={isStreaming}
/>
) : null}
<div className="my-1 min-w-0 max-w-full wrap-break-word">
<MemoizedMarkdown
content={displayContent || " "}
streaming={isStreaming && message.role === "assistant"}
/>
</div>
<div className="my-1 min-w-0 max-w-full wrap-break-word">
<MemoizedMarkdown
content={displayContent || " "}
streaming={isStreaming && message.role === "assistant"}
/>
</div>
{shouldRenderUserActions ? (
<div className="space-y-1">
<div className="flex h-6 items-center justify-end">
<div
className={cn(
"flex items-center justify-end gap-2",
keepUserActionsVisible
? "pointer-events-auto opacity-100"
: hiddenActionButtonsClassName,
)}
</MessageContent>
{shouldRenderUserActions ? (
<>
<MessageActions visible={keepUserActionsVisible}>
{onCopyRawText ? (
<MessageAction
label={wasCopied ? "Copied user message" : "Copy user message"}
onClick={onCopyRawText}
title={wasCopied ? "Copied" : "Copy message"}
>
{onCopyRawText ? (
<Button
className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground"
aria-label={
wasCopied ? "Copied user message" : "Copy user message"
}
onClick={onCopyRawText}
size="sm"
title={wasCopied ? "Copied" : "Copy message"}
type="button"
variant="ghost"
>
{wasCopied ? (
<Check className="h-3.5 w-3.5" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
</Button>
) : null}
{checkpoint ? (
<Button
className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground"
aria-label="Restore checkpoint"
disabled={restoreDisabled || restorePending}
onClick={() => onRestoreCheckpoint?.(checkpoint.runCount)}
size="sm"
title="Restore checkpoint"
type="button"
variant="ghost"
>
{restorePending ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<UndoIcon className="h-3.5 w-3.5" />
)}
</Button>
) : null}
</div>
</div>
{restoreError ? (
<div className="text-right text-xs text-destructive">
{restoreError}
</div>
{wasCopied ? (
<Check className="h-3.5 w-3.5" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
</MessageAction>
) : null}
</div>
) : null}
{shouldRenderAssistantActions ? (
<div className="flex h-6 items-center hidden">
<div
className={cn(
"flex items-center gap-0",
keepAssistantActionsVisible
? "pointer-events-auto opacity-100"
: hiddenActionButtonsClassName,
)}
>
{onCopyRawText ? (
<Button
className="h-6 gap-1.5 px-2 text-[11px] text-muted-foreground hover:text-foreground"
aria-label={
wasCopied
? "Copied assistant message"
: "Copy assistant message"
}
onClick={onCopyRawText}
size="sm"
title={wasCopied ? "Copied" : "Copy raw assistant output"}
type="button"
variant="ghost"
>
{wasCopied ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
) : null}
{onForkSession ? (
<Button
className="h-6 gap-1.5 px-2 text-[11px] text-muted-foreground hover:text-foreground"
aria-label="Fork session"
disabled={forkPending}
onClick={onForkSession}
size="sm"
title="Fork session - copy full message history into a new session"
type="button"
variant="ghost"
>
{forkPending ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<SplitIcon className="h-3 w-3" />
)}
</Button>
) : null}
{forkError ? (
<span className="text-[11px] text-destructive">
{forkError}
</span>
) : null}
{checkpoint ? (
<MessageAction
disabled={restoreDisabled || restorePending}
label="Restore checkpoint"
onClick={() => onRestoreCheckpoint?.(checkpoint.runCount)}
title="Restore checkpoint"
>
{restorePending ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<UndoIcon className="h-3.5 w-3.5" />
)}
</MessageAction>
) : null}
</MessageActions>
{restoreError ? (
<div className="text-right text-xs text-destructive">
{restoreError}
</div>
</div>
) : null}
</div>
</div>
) : null}
</>
) : null}
{shouldRenderAssistantActions ? (
<MessageActions visible={keepAssistantActionsVisible}>
{onCopyRawText ? (
<MessageAction
label={
wasCopied
? "Copied assistant message"
: "Copy assistant message"
}
onClick={onCopyRawText}
title={wasCopied ? "Copied" : "Copy raw assistant output"}
>
{wasCopied ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</MessageAction>
) : null}
{onForkSession ? (
<MessageAction
disabled={forkPending}
label="Fork session"
onClick={onForkSession}
title="Fork session - copy full message history into a new session"
>
{forkPending ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<SplitIcon className="h-3 w-3" />
)}
</MessageAction>
) : null}
{forkError ? (
<span className="text-[11px] text-destructive">{forkError}</span>
) : null}
</MessageActions>
) : null}
</AgentMessage>
);
}
@@ -925,48 +797,18 @@ function ReasoningBlock({
redacted: boolean;
streaming?: boolean;
}) {
const [expanded, setExpanded] = useState(false);
const panelId = useId();
const displayContent = content || (redacted ? "[redacted]" : "");
if (!displayContent) {
return null;
}
return (
<div className="my-2">
<Button
aria-controls={panelId}
aria-expanded={expanded}
className="h-auto min-h-0 max-w-full justify-start gap-2 whitespace-normal px-0 py-1 text-left text-sm font-medium text-foreground/70 hover:bg-transparent hover:text-foreground has-[>svg]:px-0 dark:hover:bg-transparent dark:hover:text-foreground"
onClick={() => setExpanded((current) => !current)}
type="button"
variant="ghost"
>
<BrainIcon aria-hidden="true" className="size-4" />
<span>{streaming ? "Thinking" : "Thought process"}</span>
<span
aria-live="polite"
className="text-xs font-normal text-muted-foreground"
>
{streaming ? "In progress" : "Complete"}
</span>
<span aria-hidden="true" className="shrink-0 text-muted-foreground">
{expanded ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
)}
</span>
</Button>
{expanded ? (
<div
className="mt-1.5 min-w-0 max-w-full rounded-lg border border-border/70 bg-muted/30 p-3 text-sm leading-relaxed text-muted-foreground"
id={panelId}
>
<MemoizedMarkdown content={displayContent} streaming={streaming} />
</div>
) : null}
</div>
<Reasoning isStreaming={streaming}>
<ReasoningTrigger />
<ReasoningContent>
<MemoizedMarkdown content={displayContent} streaming={streaming} />
</ReasoningContent>
</Reasoning>
);
}
@@ -1347,8 +1189,6 @@ function buildToolSummaryFromMeta(
}
function ToolMessageBlock({ message }: { message: ChatMessage }) {
const [expanded, setExpanded] = useState(false);
const panelId = useId();
const payload = parseToolPayload(message.content);
const toolName = message.meta?.toolName || payload?.toolName || "tool";
const hookEventName = message.meta?.hookEventName;
@@ -1380,94 +1220,50 @@ function ToolMessageBlock({ message }: { message: ChatMessage }) {
const resultPreview = payload?.isError ? formatToolValue(payload.result) : "";
const hasExpandedSections =
details.length > 0 || Boolean(inputPreview || resultPreview);
const summaryContent = (
<>
{payload?.isError ? (
<AlertCircle className="size-4 text-destructive/80" />
) : (
<Icon className="size-4" />
)}
<span className="min-w-0 wrap-break-word">{summary.label}</span>
{summary.diff ? (
<span className="shrink-0 font-mono text-xs">
<span className="text-chart-2">+{summary.diff.additions}</span>{" "}
<span className="text-destructive">-{summary.diff.deletions}</span>
</span>
) : null}
</>
);
return (
<div className="my-2 flex w-full min-w-0 justify-start">
<div
className={cn("min-w-0 max-w-full overflow-hidden rounded-xl text-sm")}
>
{hasExpandedSections ? (
<Button
aria-controls={panelId}
aria-expanded={expanded}
className="h-auto min-h-0 max-w-full justify-start gap-2 whitespace-normal px-0 py-1 text-left text-sm font-medium text-primary hover:bg-transparent hover:text-primary/80 has-[>svg]:px-0 dark:hover:bg-transparent dark:hover:text-primary/80"
onClick={() => setExpanded((current) => !current)}
type="button"
variant="ghost"
>
{summaryContent}
<span className="shrink-0 text-muted-foreground">
{expanded ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
)}
</span>
</Button>
) : (
<div className="flex max-w-full items-center justify-start gap-2 py-1 text-left text-sm font-medium text-primary">
{summaryContent}
</div>
)}
{expanded ? (
<div
className="mt-1.5 min-w-0 max-w-full overflow-x-hidden pl-8 text-sm text-muted-foreground"
id={panelId}
>
{hasExpandedSections ? (
<div className="space-y-1">
{details.map((detail) => (
<div
className="wrap-break-word"
key={`${message.id}_${detail}`}
>
{detail}
</div>
))}
</div>
) : null}
{inputPreview ? (
<div className="space-y-1">
<div className="text-[11px] uppercase tracking-wide text-muted-foreground/80">
Input
</div>
<pre className="max-h-52 max-w-full overflow-x-hidden overflow-y-auto whitespace-pre-wrap wrap-break-word rounded-md border border-border/70 bg-background/60 p-2 text-sm leading-relaxed text-foreground">
{inputPreview}
</pre>
</div>
) : null}
{resultPreview ? (
payload?.isError ? (
<div className="mt-1">
<span className="text-destructive">{resultPreview}</span>
</div>
) : (
<div className="space-y-1">
<pre className="max-h-64 max-w-full overflow-x-hidden overflow-y-auto whitespace-pre-wrap wrap-break-word rounded-md border border-border/70 bg-background/60 p-2 text-sm leading-relaxed text-foreground">
{resultPreview}
</pre>
</div>
)
) : null}
<ToolActivity expandable={hasExpandedSections}>
<ToolActivityTrigger
additions={summary.diff?.additions}
deletions={summary.diff?.deletions}
icon={
payload?.isError ? (
<AlertCircle className="size-4 text-destructive/80" />
) : (
<Icon className="size-4" />
)
}
label={summary.label}
status={payload?.isError ? "error" : inProgress ? "running" : "success"}
/>
<ToolActivityContent>
{details.length > 0 ? (
<ToolActivityDetails>
{details.map((detail) => (
<div key={`${message.id}_${detail}`}>{detail}</div>
))}
</ToolActivityDetails>
) : null}
{inputPreview ? (
<div className="space-y-1">
<div className="text-[11px] uppercase tracking-wide text-muted-foreground/80">
Input
</div>
<ToolActivityCode className="text-sm">
{inputPreview}
</ToolActivityCode>
</div>
) : null}
</div>
</div>
{resultPreview ? (
payload?.isError ? (
<div className="mt-1 text-destructive">{resultPreview}</div>
) : (
<ToolActivityCode className="max-h-64 text-sm">
{resultPreview}
</ToolActivityCode>
)
) : null}
</ToolActivityContent>
</ToolActivity>
);
}
+3
View File
@@ -16,6 +16,9 @@ service TaskService {
rpc cancelQueuedPrompt(StringRequest) returns (Empty);
// Cancels the currently running background command
rpc cancelBackgroundCommand(EmptyRequest) returns (Empty);
// Detaches the running foreground terminal command ("Proceed While Running"):
// the agent receives the partial output and a log file path for the rest.
rpc proceedWhileRunningCommand(EmptyRequest) returns (Empty);
// Clears the current task
rpc clearTask(EmptyRequest) returns (Empty);
// Gets the total size of all tasks
@@ -56,4 +56,16 @@ describe("parseYamlFrontmatter", () => {
expect(result.data).to.deep.equal({ count: 42, enabled: true, tags: ["a", "b"] })
expect(result.body.trim()).to.equal("Content")
})
// Regression test for https://github.com/cline/cline/issues/12151
// A leading UTF-8 BOM (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not
// prevent frontmatter from being recognized.
it("parses frontmatter correctly when the content has a leading UTF-8 BOM", () => {
const input = `\uFEFF---\nname: my-skill\ndescription: A test skill\n---\n# my-skill\nThis is a test skill.`
const result = parseYamlFrontmatter(input)
expect(result.hadFrontmatter).to.equal(true)
expect(result.parseError).to.equal(undefined)
expect(result.data).to.deep.equal({ name: "my-skill", description: "A test skill" })
expect(result.body.trim()).to.equal("# my-skill\nThis is a test skill.")
})
})
@@ -139,6 +139,34 @@ Instructions here`)
expect(skills[0].source).to.equal("global")
})
// Regression test for https://github.com/cline/cline/issues/12151:
// SKILL.md files saved with a UTF-8 BOM (e.g. by Windows Notepad's "UTF-8 with BOM"
// encoding) were silently skipped because the frontmatter regex required "---" at the
// very start of the file and never accounted for the leading \uFEFF byte sequence.
it("should discover skills whose SKILL.md starts with a UTF-8 BOM", async () => {
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-skill")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["my-skill"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`\uFEFF---
name: my-skill
description: A test skill
---
# my-skill
This is a test skill.`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(1)
expect(skills[0].name).to.equal("my-skill")
expect(skills[0].description).to.equal("A test skill")
expect(skills[0].source).to.equal("global")
})
it("should discover skills from project .clinerules/skills directory", async () => {
const projectSkillsDir = path.join(TEST_CWD, ".clinerules", "skills")
const skillDir = path.join(projectSkillsDir, "explaining-code")
@@ -1,3 +1,4 @@
import { stripUtf8Bom } from "@cline/shared"
import * as yaml from "js-yaml"
export type FrontmatterParseResult = {
@@ -35,11 +36,16 @@ export type FrontmatterParseResult = {
* - If no frontmatter exists, returns data={} and body=original markdown.
*/
export function parseYamlFrontmatter(markdown: string): FrontmatterParseResult {
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
// regex below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
const normalizedMarkdown = stripUtf8Bom(markdown)
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
const match = markdown.match(frontmatterRegex)
const match = normalizedMarkdown.match(frontmatterRegex)
if (!match) {
return { data: {}, body: markdown, hadFrontmatter: false }
return { data: {}, body: normalizedMarkdown, hadFrontmatter: false }
}
const [, yamlContent, body] = match
@@ -48,6 +54,6 @@ export function parseYamlFrontmatter(markdown: string): FrontmatterParseResult {
return { data, body, hadFrontmatter: true }
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return { data: {}, body: markdown, hadFrontmatter: true, parseError: message }
return { data: {}, body: normalizedMarkdown, hadFrontmatter: true, parseError: message }
}
}
@@ -25,6 +25,7 @@ export async function getStateToPostToWebview(controller: {
mcpHub?: any
backgroundCommandRunning?: boolean
backgroundCommandTaskId?: string
foregroundCommandRunning?: boolean
workspaceManager?: any
checkpointRestoreInput?: ExtensionState["checkpointRestoreInput"]
}): Promise<ExtensionState> {
@@ -157,6 +158,7 @@ export async function getStateToPostToWebview(controller: {
favoritedModelIds,
backgroundCommandRunning: controller.backgroundCommandRunning ?? false,
backgroundCommandTaskId: controller.backgroundCommandTaskId,
foregroundCommandRunning: controller.foregroundCommandRunning ?? false,
workspaceRoots: controller.workspaceManager?.getRoots?.() ?? [],
primaryRootIndex: controller.workspaceManager?.getPrimaryIndex?.() ?? 0,
isMultiRootWorkspace: (controller.workspaceManager?.getRoots?.()?.length ?? 0) > 1,
@@ -239,7 +239,10 @@ export async function updateSettings(controller: Controller, request: UpdateSett
controller.stateManager.setGlobalState("defaultTerminalProfile", request.defaultTerminalProfile)
// Update the live terminal manager so new terminals use the new profile.
// Existing terminals are left open — they're keyed by effective shell
// and reused when compatible, or skipped when not.
// and reused when compatible, or skipped when not. No session rebuild
// is needed: the run_commands tool re-reads the profile each time a
// model request is built, so the description and execution both pick
// up the new shell at the next request boundary.
controller.terminalManager?.setDefaultTerminalProfile(request.defaultTerminalProfile)
}
@@ -192,7 +192,10 @@ export async function updateSettingsCli(controller: Controller, request: UpdateS
controller.stateManager.setGlobalState("defaultTerminalProfile", defaultTerminalProfile)
// Update the live terminal manager so new terminals use the new profile.
// Existing terminals are left open — they're keyed by effective shell
// and reused when compatible, or skipped when not.
// and reused when compatible, or skipped when not. No session rebuild
// is needed: the run_commands tool re-reads the profile each time a
// model request is built, so the description and execution both pick
// up the new shell at the next request boundary.
controller.terminalManager?.setDefaultTerminalProfile(defaultTerminalProfile)
}
}
@@ -0,0 +1,15 @@
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
import { Controller } from ".."
/**
* "Proceed While Running": detach the in-flight foreground terminal command(s)
* so the agent turn continues with the partial output while the commands keep
* running in the user's terminal, streaming further output to a log file.
*/
export async function proceedWhileRunningCommand(controller: Controller, _request: EmptyRequest): Promise<Empty> {
const controllerWithProceed = controller as Controller & {
proceedWhileRunningCommand: () => Promise<void>
}
await controllerWithProceed.proceedWhileRunningCommand()
return Empty.create()
}
+5 -4
View File
@@ -15,8 +15,9 @@ Designed to be driven from an agentic loop via `curl` commands.
## Quick Start
```bash
# Terminal 1: Start the debug harness server
bun src/dev/debug-harness/server.ts --auto-launch --skip-build
# Terminal 1: Start the debug harness server.
# Run with node, NOT bun — Playwright's Electron launch times out under bun.
node src/dev/debug-harness/server.ts --auto-launch --skip-build
# Terminal 2: Interact via curl
curl localhost:19229/api -d '{"method":"status"}'
@@ -27,7 +28,7 @@ curl localhost:19229/api -d '{"method":"ui.screenshot"}'
## Server Options
```
bun src/dev/debug-harness/server.ts [options]
node src/dev/debug-harness/server.ts [options]
Options:
--skip-build Skip building extension/webview (use existing dist/)
@@ -42,7 +43,7 @@ Options:
```bash
# This builds protos, extension (unminified+sourcemaps), webview (unminified+sourcemaps),
# downloads VSCode, launches it, and connects CDP to the extension host.
bun src/dev/debug-harness/server.ts --auto-launch
node src/dev/debug-harness/server.ts --auto-launch
```
## Data Isolation
+15 -9
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env node
/**
* Debug Harness Server
@@ -10,7 +10,12 @@
* - UI automation (click, type, screenshot) via Playwright
*
* Usage:
* bun src/dev/debug-harness/server.ts [options]
* node src/dev/debug-harness/server.ts [options]
*
* Run with node, not bun: Playwright's _electron.launch() never finishes
* attaching to the debugee under bun (the Electron process starts, but the
* launch times out), while the same launch works under node. Node >= 22.6
* runs this file directly via type stripping.
*
* Options:
* --skip-build Skip building extension/webview
@@ -39,7 +44,6 @@ import path from "node:path"
import { fileURLToPath } from "node:url"
import { downloadAndUnzipVSCode, SilentReporter } from "@vscode/test-electron"
import { _electron, type CDPSession, type ElectronApplication, type Frame, type Page } from "playwright"
import WebSocket from "ws"
const __script_dir = typeof __dirname !== "undefined" ? __dirname : path.dirname(fileURLToPath(import.meta.url))
@@ -201,19 +205,21 @@ class CdpClient {
async connect(wsUrl: string): Promise<void> {
return new Promise((resolve, reject) => {
// The runtime's built-in WebSocket (browser-style events), so the
// harness has no dependency on the `ws` package.
const ws = new WebSocket(wsUrl)
ws.on("open", () => {
ws.addEventListener("open", () => {
this.ws = ws
resolve()
})
ws.on("error", (e: Error) => {
if (!this.ws) reject(e)
ws.addEventListener("error", () => {
if (!this.ws) reject(new Error(`WebSocket connection failed: ${wsUrl}`))
})
ws.on("close", () => {
ws.addEventListener("close", () => {
this.ws = null
})
ws.on("message", (raw: WebSocket.Data) => {
const msg = JSON.parse(raw.toString())
ws.addEventListener("message", (event: MessageEvent) => {
const msg = JSON.parse(typeof event.data === "string" ? event.data : Buffer.from(event.data).toString())
if (msg.id !== undefined) {
const p = this.pending.get(msg.id)
if (p) {
@@ -262,10 +262,15 @@ export class VscodeTerminalManager {
return mergePromise(process, promise)
}
async getOrCreateTerminal(cwd: string): Promise<ITerminalInfo> {
/**
* @param profileId Terminal profile to create/match the terminal with.
* Defaults to the current setting; callers that captured the profile
* earlier (e.g. when the model request was built) pass it here so a
* settings change does not switch shells under an in-flight tool call.
*/
async getOrCreateTerminal(cwd: string, profileId: string = this.defaultTerminalProfile): Promise<ITerminalInfo> {
const terminals = TerminalRegistry.getAllTerminals()
const expectedShellPath =
this.defaultTerminalProfile !== "default" ? getShellForProfile(this.defaultTerminalProfile) : undefined
const expectedShellPath = profileId !== "default" ? getShellForProfile(profileId) : undefined
// Resolve effective shell for comparison (so "default" and "zsh" match on macOS)
const effectiveExpected = VscodeTerminalManager.effectiveShellPath(expectedShellPath)
@@ -694,4 +694,41 @@ describe("TerminalProcess (Integration Tests)", () => {
;(emitSpy as sinon.SinonSpy).calledWith("line", "line 3 continued").should.be.true()
processAny.buffer.should.equal("")
})
it("detach emits continue but keeps line listeners attached and listening", () => {
const processAny = process as any
const continueEvents: number[] = []
const lines: string[] = []
process.on("continue", () => continueEvents.push(1))
process.on("line", (line) => lines.push(line))
process.detach()
continueEvents.length.should.equal(1)
// Unlike continue(), detach must not stop listening or drop 'line'
// listeners: output after detach still reaches subscribers (this is
// what streams the rest of a detached command to the log file).
processAny.isListening.should.be.true()
processAny.emitIfEol("after detach\n")
lines.should.containEql("after detach")
})
it("detach flushes a buffered partial line before emitting continue", () => {
const processAny = process as any
const events: string[] = []
process.on("continue", () => events.push("continue"))
process.on("line", (line) => events.push(`line:${line}`))
// A chunk with no trailing newline stays in the internal buffer.
processAny.emitIfEol("partial output")
processAny.buffer.should.equal("partial output")
process.detach()
// The partial line must reach listeners before 'continue' resolves the
// awaited promise; otherwise it is missing from the partial output and
// from the log's initial flush.
events.should.eql(["line:partial output", "continue"])
processAny.buffer.should.equal("")
})
})
@@ -16,8 +16,8 @@ import {
TRUNCATE_KEEP_LINES,
} from "@/integrations/terminal/constants"
import type { ITerminalProcess, TerminalCompletionDetails, TerminalProcessEvents } from "@/integrations/terminal/types"
import { Logger } from "@/shared/services/Logger"
import type { MarkerlessCompletionCause } from "@/services/telemetry/TelemetryService"
import { Logger } from "@/shared/services/Logger"
import { Osc633EventType, Osc633Parser } from "./osc633Parser"
import { classifyShellPrompt, getLastLine } from "./shellPromptHeuristics"
@@ -522,6 +522,23 @@ export class VscodeTerminalProcess extends EventEmitter<TerminalProcessEvents> i
this.emit("continue")
}
/**
* Resolve the awaited promise while the command keeps running ("Proceed
* While Running"). Listeners stay attached and 'line' events keep
* flowing — unlike continue() — so callers can stream the remaining
* output until the command actually completes. Because 'completed' is
* only emitted by the read loop when the command genuinely ends, the
* terminal stays busy and is not eligible for reuse until then.
*/
detach() {
// Flush any partial line (no trailing newline yet) so it reaches
// listeners before the awaited promise resolves; otherwise it would be
// dropped from both the partial output and the log capture if the
// command exits without further newline-terminated output.
this.emitRemainingBufferIfListening()
this.emit("continue")
}
/**
* Get output that hasn't been retrieved yet.
* Truncates if output is too large to prevent context window overflow.
@@ -63,10 +63,17 @@ export interface ITerminalProcess extends EventEmitter<TerminalProcessEvents> {
/**
* Continue execution without waiting for completion.
* Stops event emission and resolves the promise.
* This is called when user clicks "Proceed While Running".
*/
continue(): void
/**
* Resolve the awaited promise while the command keeps running ("Proceed
* While Running"). Unlike continue(), output listeners stay attached and
* 'line'/'completed' events keep flowing, so callers can stream the rest
* of the output (e.g. to a log file) until the command completes.
*/
detach(): void
/**
* Get output that hasn't been retrieved yet.
* @returns The unretrieved output
+25
View File
@@ -68,6 +68,7 @@ import {
import { SdkCompactionCoordinator } from "./sdk-compaction-coordinator"
import { SdkDiffEditCoordinator } from "./sdk-diff-edit-coordinator"
import { SdkFollowupCoordinator } from "./sdk-followup-coordinator"
import { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
import { SdkInteractionCoordinator } from "./sdk-interaction-coordinator"
import { SdkMcpCoordinator } from "./sdk-mcp-coordinator"
import { SdkMessageCoordinator, type SessionEventListener } from "./sdk-message-coordinator"
@@ -204,6 +205,15 @@ export class Controller {
// standalone (JetBrains/CLI) host run commands through the SDK's built-in tool.
private _terminalManager?: VscodeTerminalManager
// Registry of in-flight foreground (VS Code terminal) command executions.
// Owned here — not by the session — so it survives session rebuilds, which
// recreate the tool set. Drives the "Proceed While Running" button.
private readonly foregroundCommands = new SdkForegroundCommandCoordinator({
onRunningChanged: () => {
void this.postStateToWebview()
},
})
// Private state kept for stub compatibility
private backgroundCommandRunning = false
private backgroundCommandTaskId?: string
@@ -330,6 +340,7 @@ export class Controller {
},
onDidBecomeIdle: () => this.handleSessionBecameIdle(),
getRemoteConfigIntegration: () => this.remoteConfigCoreIntegration,
foregroundCommands: this.foregroundCommands,
getTerminalManager: () => {
// Guarded by getEffectiveTerminalExecutionMode() at the read sites
// (vscode-session-host.ts, sdk-terminal-execution-mode-coordinator.ts):
@@ -1174,6 +1185,19 @@ export class Controller {
stubWarn("cancelBackgroundCommand")
}
/**
* "Proceed While Running": detach every in-flight foreground terminal
* command. Each pending run_commands call returns its partial output plus
* the log file path the remaining output is redirected to, and the agent
* turn continues while the commands keep running in their terminals.
*/
async proceedWhileRunningCommand(): Promise<void> {
const detached = this.foregroundCommands.proceedWhileRunning()
if (detached === 0) {
Logger.warn("[SdkController] proceedWhileRunningCommand: No foreground command is running")
}
}
async cancelQueuedPrompt(promptId: string): Promise<void> {
const trimmedPromptId = promptId.trim()
if (!trimmedPromptId) {
@@ -1868,6 +1892,7 @@ export class Controller {
mcpHub: this.mcpHub,
backgroundCommandRunning: this.backgroundCommandRunning,
backgroundCommandTaskId: this.backgroundCommandTaskId,
foregroundCommandRunning: this.foregroundCommands.isRunning,
})
const sdkTaskHistory = (await this.taskHistory.listHistory({ limit: 100, hydrate: false }))
.map(sessionHistoryRecordToHistoryItem)
@@ -454,13 +454,35 @@ describe("SdkDiffEditCoordinator", () => {
expect(callOrder).toEqual(["close", "apply"])
})
it("applies patches without preview sessions directly", async () => {
it("shows a brief preview around auto-approved patches", async () => {
await writeFile("patched.ts", "line one\nline two\n")
const patch = ["*** Begin Patch", "*** Update File: patched.ts", "@@", "-line one", "+line ONE", "*** End Patch"].join(
"\n",
)
const result = await coordinator.executeApplyPatchTool({ input: patch }, tempDir, makeContext("tc9"))
expect(result).toBe("fallback apply_patch result")
expect(fallbackApplyPatch).toHaveBeenCalledOnce()
expect(previews).toHaveLength(1)
expect(previews[0].opened).toMatchObject({
absolutePath: path.join(tempDir, "patched.ts"),
leftContent: "line one\nline two\n",
rightContent: "line ONE\nline two\n",
})
expect(previews[0].closed).toBe(1)
})
it("applies auto-approved patches without a preview when background edit is enabled", async () => {
backgroundEdit = true
const result = await coordinator.executeApplyPatchTool(
{ input: "*** Begin Patch\n*** End Patch" },
tempDir,
makeContext("tc9"),
)
expect(result).toBe("fallback apply_patch result")
expect(fallbackApplyPatch).toHaveBeenCalledOnce()
expect(previews).toHaveLength(0)
})
})
@@ -122,12 +122,32 @@ export class SdkDiffEditCoordinator {
}
/**
* The `apply_patch` tool executor override: close the preview, then delegate the
* whole patch application to the SDK's default executor.
* The `apply_patch` tool executor override: manually-approved patches close their
* approval preview before applying; auto-approved patches show a brief preview
* around execution, matching the `editor` tool behavior.
*/
async executeApplyPatchTool(input: ApplyPatchInput, cwd: string, context: AgentToolContext): Promise<string> {
await this.discardPreview(context.toolCallId ?? "")
return this.fallbackApplyPatchExecutor(input, cwd, context)
const toolCallId = context.toolCallId ?? ""
const hadPreApprovalPreview = this.sessions.has(toolCallId)
try {
if (hadPreApprovalPreview) {
await this.discardPreview(toolCallId)
} else if (!this.options.isBackgroundEditEnabled()) {
try {
await this.openPatchPreview(toolCallId, input)
} catch (error) {
Logger.warn(`[SdkDiffEditCoordinator] Failed to show auto-approve patch preview: ${error}`)
}
}
const result = await this.fallbackApplyPatchExecutor(input, cwd, context)
if (!hadPreApprovalPreview && this.sessions.get(toolCallId)?.preview) {
await lingerDelay(this.autoApprovePreviewLingerMs, context.signal)
}
return result
} finally {
await this.discardPreview(toolCallId)
}
}
/** Closes one preview (reject / abort / edit applied). Never throws; unknown ids are a no-op. */
@@ -0,0 +1,74 @@
import { describe, expect, it, vi } from "vitest"
import { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
describe("SdkForegroundCommandCoordinator", () => {
it("reports isRunning while a handle is registered and notifies on changes", () => {
const onRunningChanged = vi.fn()
const coordinator = new SdkForegroundCommandCoordinator({ onRunningChanged })
expect(coordinator.isRunning).toBe(false)
const unregister = coordinator.register({ detach: () => {} })
expect(coordinator.isRunning).toBe(true)
expect(onRunningChanged).toHaveBeenCalledWith(true)
unregister()
expect(coordinator.isRunning).toBe(false)
expect(onRunningChanged).toHaveBeenCalledWith(false)
})
it("only notifies on actual transitions, not per handle", () => {
const onRunningChanged = vi.fn()
const coordinator = new SdkForegroundCommandCoordinator({ onRunningChanged })
const unregister1 = coordinator.register({ detach: () => {} })
const unregister2 = coordinator.register({ detach: () => {} })
expect(onRunningChanged).toHaveBeenCalledTimes(1)
unregister1()
expect(onRunningChanged).toHaveBeenCalledTimes(1)
unregister2()
expect(onRunningChanged).toHaveBeenCalledTimes(2)
})
it("unregister is idempotent", () => {
const onRunningChanged = vi.fn()
const coordinator = new SdkForegroundCommandCoordinator({ onRunningChanged })
const unregister = coordinator.register({ detach: () => {} })
unregister()
unregister()
expect(onRunningChanged).toHaveBeenCalledTimes(2)
})
it("proceedWhileRunning detaches every registered handle and reports the count", () => {
const coordinator = new SdkForegroundCommandCoordinator()
const detach1 = vi.fn()
const detach2 = vi.fn()
coordinator.register({ detach: detach1 })
coordinator.register({ detach: detach2 })
expect(coordinator.proceedWhileRunning()).toBe(2)
expect(detach1).toHaveBeenCalledTimes(1)
expect(detach2).toHaveBeenCalledTimes(1)
})
it("proceedWhileRunning is a no-op returning 0 when nothing is running", () => {
const coordinator = new SdkForegroundCommandCoordinator()
expect(coordinator.proceedWhileRunning()).toBe(0)
})
it("proceedWhileRunning survives a handle whose detach throws", () => {
const coordinator = new SdkForegroundCommandCoordinator()
const detach2 = vi.fn()
coordinator.register({
detach: () => {
throw new Error("boom")
},
})
coordinator.register({ detach: detach2 })
expect(coordinator.proceedWhileRunning()).toBe(2)
expect(detach2).toHaveBeenCalledTimes(1)
})
})
@@ -0,0 +1,80 @@
/**
* Tracks in-flight foreground (VS Code terminal) command executions so the
* "Proceed While Running" button can detach them: each pending tool call
* returns with its partial output while the command keeps running in the
* user's terminal, streaming further output to a log file.
*
* Owned by SdkController so it outlives session rebuilds (which recreate the
* tool set and its reused executor closure). Handles are registered per tool
* invocation never on the reused executor so parallel commands in one
* tool call each get their own handle and log file.
*/
import { Logger } from "@/shared/services/Logger"
export interface ForegroundCommandHandle {
/**
* Stop waiting for the command: flush the output captured so far to a
* log file, keep appending until the command completes, and resolve the
* pending tool execution with the partial output. Idempotent.
*/
detach(): void
}
export interface SdkForegroundCommandCoordinatorOptions {
/** Called whenever isRunning flips; used to push the flag to the webview. */
onRunningChanged?: (running: boolean) => void
}
export class SdkForegroundCommandCoordinator {
private readonly handles = new Set<ForegroundCommandHandle>()
constructor(private readonly options: SdkForegroundCommandCoordinatorOptions = {}) {}
/** Whether any foreground command is currently awaited by a tool call. */
get isRunning(): boolean {
return this.handles.size > 0
}
/**
* Track one in-flight foreground execution. Returns an unregister
* function the caller must invoke when the execution settles (completes,
* fails, aborts, or detaches) typically from a `finally` block.
*/
register(handle: ForegroundCommandHandle): () => void {
const wasRunning = this.isRunning
this.handles.add(handle)
this.notifyIfChanged(wasRunning)
return () => {
const wasRunningBefore = this.isRunning
if (this.handles.delete(handle)) {
this.notifyIfChanged(wasRunningBefore)
}
}
}
/**
* Detach every in-flight foreground command ("Proceed While Running").
* Each pending tool execution resolves with its partial output and log
* file path; the commands keep running in their terminals.
*
* @returns the number of commands detached (0 when none were running).
*/
proceedWhileRunning(): number {
const handles = [...this.handles]
for (const handle of handles) {
try {
handle.detach()
} catch (error) {
Logger.error("[ForegroundCommands] Failed to detach foreground command:", error)
}
}
return handles.length
}
private notifyIfChanged(wasRunning: boolean): void {
if (this.isRunning !== wasRunning) {
this.options.onRunningChanged?.(this.isRunning)
}
}
}
@@ -12,6 +12,7 @@ import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTermin
import { McpHub } from "@/services/mcp/McpHub"
import { Logger } from "@/shared/services/Logger"
import type { ActiveSession } from "./cline-session-factory"
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
import { buildToolPolicies } from "./sdk-tool-policies"
import type { SdkSessionHost } from "./session-host"
import { VscodeSessionHost } from "./vscode-session-host"
@@ -32,6 +33,8 @@ export interface SdkSessionLifecycleOptions {
onSessionEvent: (event: CoreSessionEvent) => void
/** Lazy factory for the VscodeTerminalManager (foreground terminal support). */
getTerminalManager?: () => VscodeTerminalManager
/** Registry of in-flight foreground executions for "Proceed While Running". */
foregroundCommands?: SdkForegroundCommandCoordinator
/** Returns the latest prepared remote-config integration, if remote config is active. */
getRemoteConfigIntegration?: () => PreparedRemoteConfigCoreIntegration | undefined
/** Shared SDK telemetry service owned by SdkController. */
@@ -322,6 +325,7 @@ export class SdkSessionLifecycle {
editorExecutor: this.options.editorExecutor,
applyPatchExecutor: this.options.applyPatchExecutor,
getTerminalManager: this.options.getTerminalManager,
foregroundCommands: this.options.foregroundCommands,
getRemoteConfigIntegration: this.options.getRemoteConfigIntegration,
telemetry: this.options.telemetry,
})
@@ -33,8 +33,11 @@ export class SdkTerminalExecutionModeCoordinator {
if (previous === next) {
return
}
this.requestRebuild(`Terminal execution mode changed: ${previous} -> ${next}`)
}
Logger.log(`[SdkController] Terminal execution mode changed: ${previous} -> ${next}`)
private requestRebuild(reason: string): void {
Logger.log(`[SdkController] ${reason}`)
const activeSession = this.options.sessions.getActiveSession()
if (!activeSession) {
@@ -1,9 +1,111 @@
import { CommandExitError } from "@cline/core"
import { EventEmitter } from "events"
import { describe, expect, it } from "vitest"
import type { TerminalCompletionDetails } from "@/integrations/terminal/types"
import * as fs from "fs"
import { afterEach, describe, expect, it, vi } from "vitest"
import * as vscode from "vscode"
import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTerminalManager"
import { executeForeground, formatCommandForTerminal } from "./vscode-run-commands-tool"
import type { TerminalCompletionDetails } from "@/integrations/terminal/types"
import { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
import {
createVscodeRunCommandsTool,
executeForeground,
formatCommandForTerminal,
PROCEED_LOG_MAX_BYTES,
} from "./vscode-run-commands-tool"
const mocks = vi.hoisted(() => ({
existsSync: vi.fn<(path: fs.PathLike) => boolean>(),
getGlobalSettingsKey: vi.fn(() => "default"),
}))
vi.mock("fs", async (importOriginal) => ({
...(await importOriginal<typeof import("fs")>()),
existsSync: mocks.existsSync,
}))
vi.mock("@/core/storage/StateManager", () => ({
StateManager: {
get: () => ({ getGlobalSettingsKey: mocks.getGlobalSettingsKey }),
},
}))
// The real telemetry proxy lazily initializes TelemetryService, which requires
// a HostProvider that unit tests don't set up.
vi.mock("@services/telemetry", () => ({
TerminalUserInterventionAction: { PROCESS_WHILE_RUNNING: "process_while_running" },
telemetryService: {
captureTerminalUserIntervention: () => {},
captureTerminalExecution: () => {},
},
}))
const originalPlatform = process.platform
const originalEnv = { ...process.env }
const originalGetConfiguration = vscode.workspace.getConfiguration
afterEach(() => {
Object.defineProperty(process, "platform", { value: originalPlatform })
process.env = { ...originalEnv }
vscode.workspace.getConfiguration = originalGetConfiguration
mocks.existsSync.mockReset()
mocks.getGlobalSettingsKey.mockReset()
mocks.getGlobalSettingsKey.mockReturnValue("default")
})
describe("createVscodeRunCommandsTool", () => {
it("constructs a cmd tool from the stock array-valued Command Prompt profile", () => {
Object.defineProperty(process, "platform", { value: "win32" })
process.env.windir = "C:\\Windows"
mocks.existsSync.mockImplementation((candidate) => candidate === "C:\\Windows\\System32\\cmd.exe")
vscode.workspace.getConfiguration = () =>
({
get: (key: string) => {
if (key === "defaultProfile.windows") {
return "Command Prompt"
}
if (key === "profiles.windows") {
return {
"Command Prompt": {
path: [`\${env:windir}\\Sysnative\\cmd.exe`, `\${env:windir}\\System32\\cmd.exe`],
},
}
}
return undefined
},
}) as never
const tool = createVscodeRunCommandsTool({
cwd: "C:\\workspace",
getTerminalManager: () => {
throw new Error("Terminal manager should not be created during tool construction")
},
vscodeTerminalExecutionMode: "vscodeTerminal",
})
expect(tool.name).toBe("run_commands")
expect(tool.description).toContain("Commands run through cmd.exe")
})
it("re-derives the description from the current profile on each read", () => {
Object.defineProperty(process, "platform", { value: "win32" })
mocks.existsSync.mockReturnValue(true)
mocks.getGlobalSettingsKey.mockReturnValue("cmd")
const tool = createVscodeRunCommandsTool({
cwd: "C:\\workspace",
getTerminalManager: () => {
throw new Error("Terminal manager should not be created during tool construction")
},
vscodeTerminalExecutionMode: "vscodeTerminal",
})
expect(tool.description).toContain("Commands run through cmd.exe")
// A profile change takes effect at the next description read (the
// model-request boundary), without a session rebuild.
mocks.getGlobalSettingsKey.mockReturnValue("powershell-7")
expect(tool.description).toContain("Commands run through PowerShell")
})
})
/**
* Minimal fake of the process object returned by VscodeTerminalManager.runCommand():
@@ -12,11 +114,13 @@ import { executeForeground, formatCommandForTerminal } from "./vscode-run-comman
*/
function createFakeTerminalProcess(options: { lines?: string[]; completionDetails?: TerminalCompletionDetails } = {}) {
const emitter = new EventEmitter()
let resolvePromise!: () => void
// Emit on a macrotask (not a microtask) so executeForeground's
// `await terminalManager.getOrCreateTerminal(cwd)` and subsequent
// `process.on("line", ...)` registration are guaranteed to run first,
// matching the ordering a real terminal process provides.
const promise = new Promise<void>((resolve) => {
resolvePromise = resolve
setTimeout(() => {
for (const line of options.lines ?? []) {
emitter.emit("line", line)
@@ -31,6 +135,10 @@ function createFakeTerminalProcess(options: { lines?: string[]; completionDetail
catch: promise.catch.bind(promise),
finally: promise.finally.bind(promise),
getCompletionDetails: () => options.completionDetails ?? {},
detach: () => {
emitter.emit("continue")
resolvePromise()
},
})
return fakeProcess as unknown as ReturnType<VscodeTerminalManager["runCommand"]>
}
@@ -42,6 +150,50 @@ function createFakeTerminalManager(process: ReturnType<VscodeTerminalManager["ru
} as unknown as VscodeTerminalManager
}
/**
* A controllable fake terminal process for detach tests: the caller decides
* when lines are emitted and when the command completes. Mirrors the real
* VscodeTerminalProcess contract: detach() resolves the awaited promise while
* 'line'/'completed' events keep flowing.
*/
function createControllableTerminalProcess() {
const emitter = new EventEmitter()
let resolvePromise!: () => void
const promise = new Promise<void>((resolve) => {
resolvePromise = resolve
})
const fakeProcess = Object.assign(emitter, {
then: promise.then.bind(promise),
catch: promise.catch.bind(promise),
finally: promise.finally.bind(promise),
getCompletionDetails: () => ({}),
detach: () => {
emitter.emit("continue")
resolvePromise()
},
})
return {
process: fakeProcess as unknown as ReturnType<VscodeTerminalManager["runCommand"]>,
emitLine: (line: string) => emitter.emit("line", line),
complete: (details?: TerminalCompletionDetails) => {
emitter.emit("completed", details)
emitter.emit("continue")
resolvePromise()
},
}
}
/** Poll until the predicate holds, for asserting on async log-file writes. */
async function waitFor(predicate: () => boolean, timeoutMs = 2000): Promise<void> {
const start = Date.now()
while (!predicate()) {
if (Date.now() - start > timeoutMs) {
throw new Error("waitFor timed out")
}
await new Promise((resolve) => setTimeout(resolve, 10))
}
}
describe("formatCommandForTerminal", () => {
it.each([
{
@@ -129,6 +281,19 @@ describe("executeForeground", () => {
expect(result).toBe("hello")
})
it("passes the caller's terminal profile through to getOrCreateTerminal", async () => {
const process = createFakeTerminalProcess({ lines: ["ok"] })
const getOrCreateTerminal = vi.fn(async () => ({ terminal: { show: () => {} } }) as never)
const terminalManager = {
getOrCreateTerminal,
runCommand: () => process,
} as unknown as VscodeTerminalManager
await executeForeground("echo ok", "/workspace", terminalManager, 1000, undefined, undefined, "wsl-bash")
expect(getOrCreateTerminal).toHaveBeenCalledWith("/workspace", "wsl-bash")
})
it("throws CommandExitError with the exit code on non-zero exit", async () => {
const terminalManager = createFakeTerminalManager(
createFakeTerminalProcess({ lines: ["boom"], completionDetails: { exitCode: 127 } }),
@@ -172,4 +337,207 @@ describe("executeForeground", () => {
expect((error as InstanceType<typeof CommandExitError>).output).toContain("Terminal closed")
}
})
it("unregisters its foreground handle when the command completes normally", async () => {
const coordinator = new SdkForegroundCommandCoordinator()
const terminalManager = createFakeTerminalManager(createFakeTerminalProcess({ lines: ["hello"] }))
const result = await executeForeground("echo hello", "/workspace", terminalManager, 1000, undefined, coordinator)
expect(result).toBe("hello")
expect(coordinator.isRunning).toBe(false)
})
})
describe("executeForeground — Proceed While Running", () => {
it("detach returns the partial output with the log file path, and later output lands in the log", async () => {
const coordinator = new SdkForegroundCommandCoordinator()
const { process, emitLine, complete } = createControllableTerminalProcess()
const terminalManager = createFakeTerminalManager(process)
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
await waitFor(() => coordinator.isRunning)
emitLine("listening on :3000")
expect(coordinator.proceedWhileRunning()).toBe(1)
const result = await resultPromise
expect(result).toContain("still running")
expect(result).toContain("listening on :3000")
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
expect(logFilePath).toBeTruthy()
// The handle is unregistered once the tool call returns.
expect(coordinator.isRunning).toBe(false)
// Output emitted after detach is appended to the log file, and
// completion closes it out with a completion marker.
emitLine("compiled successfully")
complete({ exitCode: 0 })
await waitFor(() => {
try {
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
} catch {
return false
}
})
const log = fs.readFileSync(logFilePath!, "utf8")
expect(log).toContain("listening on :3000") // buffered lines flushed at detach
expect(log).toContain("compiled successfully") // streamed after detach
fs.rmSync(logFilePath!, { force: true })
})
it("detaches each parallel command into its own log file", async () => {
const coordinator = new SdkForegroundCommandCoordinator()
const first = createControllableTerminalProcess()
const second = createControllableTerminalProcess()
const firstPromise = executeForeground(
"first-cmd",
"/workspace",
createFakeTerminalManager(first.process),
100_000,
undefined,
coordinator,
)
const secondPromise = executeForeground(
"second-cmd",
"/workspace",
createFakeTerminalManager(second.process),
100_000,
undefined,
coordinator,
)
await waitFor(() => coordinator.isRunning)
first.emitLine("first output")
second.emitLine("second output")
expect(coordinator.proceedWhileRunning()).toBe(2)
const [firstResult, secondResult] = await Promise.all([firstPromise, secondPromise])
const firstLog = /redirected to this file[^:]*: (.+)$/m.exec(firstResult)?.[1]?.trim()
const secondLog = /redirected to this file[^:]*: (.+)$/m.exec(secondResult)?.[1]?.trim()
expect(firstLog).toBeTruthy()
expect(secondLog).toBeTruthy()
expect(firstLog).not.toBe(secondLog)
expect(coordinator.isRunning).toBe(false)
first.complete()
second.complete()
await waitFor(() => {
try {
return (
fs.readFileSync(firstLog!, "utf8").includes("[Command completed]") &&
fs.readFileSync(secondLog!, "utf8").includes("[Command completed]")
)
} catch {
return false
}
})
expect(fs.readFileSync(firstLog!, "utf8")).toContain("first output")
expect(fs.readFileSync(secondLog!, "utf8")).toContain("second output")
fs.rmSync(firstLog!, { force: true })
fs.rmSync(secondLog!, { force: true })
})
it("stops logging before a line that would exceed the size cap", async () => {
const coordinator = new SdkForegroundCommandCoordinator()
const { process, emitLine, complete } = createControllableTerminalProcess()
const terminalManager = createFakeTerminalManager(process)
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
await waitFor(() => coordinator.isRunning)
expect(coordinator.proceedWhileRunning()).toBe(1)
const result = await resultPromise
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
expect(logFilePath).toBeTruthy()
// A single line larger than the whole cap must not be written at all —
// the cap is checked before writing, so one huge line (e.g. a dumped
// blob) cannot blow the log far past PROCEED_LOG_MAX_BYTES.
emitLine("small line before the blob")
emitLine("x".repeat(PROCEED_LOG_MAX_BYTES))
emitLine("after the cap")
complete({ exitCode: 0 })
await waitFor(() => {
try {
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
} catch {
return false
}
})
const log = fs.readFileSync(logFilePath!, "utf8")
expect(log).toContain("small line before the blob")
expect(log).toContain(`[Log size cap of ${PROCEED_LOG_MAX_BYTES} bytes reached`)
expect(log).not.toContain("xxxx")
expect(log).not.toContain("after the cap")
expect(log.length).toBeLessThan(PROCEED_LOG_MAX_BYTES)
fs.rmSync(logFilePath!, { force: true })
})
it("applies the size cap to lines buffered before detach", async () => {
const coordinator = new SdkForegroundCommandCoordinator()
const { process, emitLine, complete } = createControllableTerminalProcess()
const terminalManager = createFakeTerminalManager(process)
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
await waitFor(() => coordinator.isRunning)
emitLine("x".repeat(PROCEED_LOG_MAX_BYTES))
expect(coordinator.proceedWhileRunning()).toBe(1)
const result = await resultPromise
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
expect(logFilePath).toBeTruthy()
complete({ exitCode: 0 })
await waitFor(() => {
try {
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
} catch {
return false
}
})
const log = fs.readFileSync(logFilePath!, "utf8")
expect(log).toContain(`[Log size cap of ${PROCEED_LOG_MAX_BYTES} bytes reached`)
expect(log).not.toContain("xxxx")
expect(Buffer.byteLength(log)).toBeLessThanOrEqual(PROCEED_LOG_MAX_BYTES)
fs.rmSync(logFilePath!, { force: true })
})
it("freezes the partial output at detach while later output still reaches the log", async () => {
const coordinator = new SdkForegroundCommandCoordinator()
const { process, emitLine, complete } = createControllableTerminalProcess()
const terminalManager = createFakeTerminalManager(process)
const resultPromise = executeForeground("devserver", "/workspace", terminalManager, 100_000, undefined, coordinator)
await waitFor(() => coordinator.isRunning)
emitLine("before detach")
expect(coordinator.proceedWhileRunning()).toBe(1)
// Emitted after detach but before the tool call's result is built:
// must appear only in the log, never in the partial output.
emitLine("after detach")
const result = await resultPromise
expect(result).toContain("before detach")
expect(result).not.toContain("after detach")
const logFilePath = /redirected to this file[^:]*: (.+)$/m.exec(result)?.[1]?.trim()
complete({ exitCode: 0 })
await waitFor(() => {
try {
return fs.readFileSync(logFilePath!, "utf8").includes("[Command completed with exit code 0]")
} catch {
return false
}
})
const log = fs.readFileSync(logFilePath!, "utf8")
expect(log).toContain("before detach")
expect(log).toContain("after detach")
fs.rmSync(logFilePath!, { force: true })
})
})
+175 -18
View File
@@ -21,12 +21,16 @@ import {
truncateCommandOutput,
} from "@cline/core"
import type { AgentTool } from "@cline/shared"
import { telemetryService } from "@services/telemetry"
import { TerminalUserInterventionAction, telemetryService } from "@services/telemetry"
import { ClineTempManager } from "@services/temp"
import * as fs from "fs"
import { StateManager } from "@/core/storage/StateManager"
import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTerminalManager"
import { MAX_UNRETRIEVED_LINES } from "@/integrations/terminal/constants"
import type { ITerminalProcess } from "@/integrations/terminal/types"
import { Logger } from "@/shared/services/Logger"
import { getShellForProfile } from "@/utils/shell"
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
// ---------------------------------------------------------------------------
// Types
@@ -38,6 +42,14 @@ type VscodeTerminalExecutionMode = "vscodeTerminal" | "backgroundExec"
/** Foreground VS Code terminals cannot be forcibly terminated; give long-running commands room to finish. */
export const VSCODE_FOREGROUND_RUN_COMMANDS_TIMEOUT_MS = 60 * 60 * 1000
/**
* Cap on the "Proceed While Running" log file. A detached devserver can log
* for days; once the cap is hit we stop appending and note the truncation.
* ClineTempManager's periodic cleanup (age + total-size caps) is the backstop
* for the files themselves.
*/
export const PROCEED_LOG_MAX_BYTES = 10 * 1024 * 1024
/** Options for creating the VSCode run_commands tool. */
export interface VscodeRunCommandsToolOptions {
/** Workspace root directory. */
@@ -48,6 +60,14 @@ export interface VscodeRunCommandsToolOptions {
bashTimeoutMs?: number
/** Terminal execution mode captured when this session's tool set is built. */
vscodeTerminalExecutionMode?: VscodeTerminalExecutionMode
/**
* Registry of in-flight foreground executions, owned by SdkController.
* When provided, each foreground command can be detached via the
* "Proceed While Running" button. Foreground-only: background (SDK
* child_process) executions cannot be detached their abort signal
* kills the process tree.
*/
foregroundCommands?: SdkForegroundCommandCoordinator
}
// ---------------------------------------------------------------------------
@@ -74,6 +94,69 @@ export function formatCommandForTerminal(command: ShellCommand): string {
return [command.command, ...(command.args ?? [])].map(quoteShellArg).join(" ")
}
/**
* Stream the rest of a detached command's output to a log file: write the
* lines buffered so far, then append each further 'line' event until
* 'completed'. The write volume is capped at PROCEED_LOG_MAX_BYTES; the
* stream is always closed by the 'completed' event, which the terminal
* process emits on every exit path (command end, Ctrl+C, terminal closed,
* markerless fallback).
*/
function beginLogCapture(process: ITerminalProcess, terminalCommand: string, existingLines: string[]): string {
const logFilePath = ClineTempManager.createTempFilePath("proceed-while-running")
const stream = fs.createWriteStream(logFilePath, { flags: "a" })
const sizeCapMessage = `[Log size cap of ${PROCEED_LOG_MAX_BYTES} bytes reached; further output is not logged.]`
stream.on("error", (error) => {
Logger.error(`[VscodeRunCommands] Failed writing proceed-while-running log ${logFilePath}:`, error)
})
let bytesWritten = 0
const tryWriteLine = (line: string): boolean => {
const chunk = `${line}\n`
const chunkBytes = Buffer.byteLength(chunk)
if (bytesWritten + chunkBytes > PROCEED_LOG_MAX_BYTES) {
return false
}
bytesWritten += chunkBytes
stream.write(chunk)
return true
}
let sizeCapReached = !tryWriteLine(`[Running command: ${terminalCommand}]`)
for (const line of existingLines) {
if (!tryWriteLine(line)) {
sizeCapReached = true
break
}
}
const onLine = (line: string): void => {
// Check the cap before writing: a single huge line (e.g. a dumped
// binary blob or minified bundle) must not blow past the cap.
if (!tryWriteLine(line)) {
tryWriteLine(sizeCapMessage)
process.removeListener("line", onLine)
}
}
if (sizeCapReached) {
tryWriteLine(sizeCapMessage)
} else {
process.on("line", onLine)
}
process.once("completed", (details) => {
process.removeListener("line", onLine)
const exitCode = details?.exitCode
tryWriteLine(
exitCode !== undefined && exitCode !== null
? `[Command completed with exit code ${exitCode}]`
: "[Command completed]",
)
stream.end()
})
return logFilePath
}
/** Exported for direct unit testing of the CommandExitError/terminalClosed mapping. */
export async function executeForeground(
command: ShellCommand,
@@ -81,9 +164,11 @@ export async function executeForeground(
terminalManager: VscodeTerminalManager,
maxOutputChars: number,
abortSignal?: AbortSignal,
foregroundCommands?: SdkForegroundCommandCoordinator,
terminalProfileId?: string,
): Promise<string> {
const terminalCommand = formatCommandForTerminal(command)
const terminalInfo = await terminalManager.getOrCreateTerminal(cwd)
const terminalInfo = await terminalManager.getOrCreateTerminal(cwd, terminalProfileId)
terminalInfo.terminal.show()
const process = terminalManager.runCommand(terminalInfo, terminalCommand)
@@ -100,7 +185,7 @@ export async function executeForeground(
// truncateCommandOutput's own head/tail strategy below — since build/test
// failures usually appear at the end of output.
const maxBufferedLines = MAX_UNRETRIEVED_LINES
process.on("line", (line: string) => {
const bufferLine = (line: string): void => {
if (outputLines.length < maxBufferedLines) {
outputLines.push(line)
} else {
@@ -108,7 +193,8 @@ export async function executeForeground(
outputLines.push(line)
droppedLines++
}
})
}
process.on("line", bufferLine)
// Handle abort signal
if (abortSignal) {
@@ -121,8 +207,33 @@ export async function executeForeground(
process.once("continue", cleanupAbortListener)
}
// Wait for completion
await process
// "Proceed While Running": register a per-invocation handle so the user
// can detach this command. Detaching redirects the remaining output to a
// log file and resolves the awaited promise; the command keeps running in
// the user's terminal (and the terminal stays busy until it completes).
let detachedLogFilePath: string | undefined
const unregister = foregroundCommands?.register({
detach: () => {
if (detachedLogFilePath !== undefined) {
return
}
detachedLogFilePath = beginLogCapture(process, terminalCommand, outputLines)
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.PROCESS_WHILE_RUNNING, "vscode")
// detach() flushes any partial line (reaching both bufferLine and
// the log) before resolving the awaited promise. After that the
// partial output is final: stop buffering so the remaining
// (log-only) output doesn't mutate outputLines while it's read.
process.detach()
process.removeListener("line", bufferLine)
},
})
try {
// Wait for completion (or detach, which also resolves the promise)
await process
} finally {
unregister?.()
}
if (abortSignal?.aborted) {
throw new Error("Command execution aborted")
}
@@ -135,6 +246,14 @@ export async function executeForeground(
maxChars: maxOutputChars,
})
if (detachedLogFilePath !== undefined) {
return [
"The user chose to proceed while the command is still running in their terminal.",
`This is partial output; further output is being redirected to this file, which you can read to check progress: ${detachedLogFilePath}`,
output.length > 0 ? `Output so far:\n${output}` : "No output so far.",
].join("\n")
}
const completionDetails = process.getCompletionDetails?.()
// A terminal closed mid-command has no exit code and no reliable output —
@@ -171,26 +290,56 @@ export async function executeForeground(
// Tool factory
// ---------------------------------------------------------------------------
/**
* The shell selected by the user's terminal profile setting at one moment:
* the profile ID for foreground terminal creation and the shell executable
* it resolves to for background spawning and description building.
*/
interface ShellSnapshot {
profileId: string
shell: string
}
/** Resolves the shell the user's terminal profile setting selects right now. */
function takeShellSnapshot(): ShellSnapshot {
// The setting is typed string, but guard empty values the same way the
// settings handlers do (they skip persisting "" but older stores may hold one).
const profileId = StateManager.get().getGlobalSettingsKey("defaultTerminalProfile") || "default"
return { profileId, shell: getShellForProfile(profileId) }
}
/**
* Creates the custom `run_commands` tool for the VSCode extension.
*
* This tool suppresses and replaces the SDK's built-in `run_commands` tool.
* The terminal execution mode is captured when the session's tool set is built.
* Switching modes rebuilds the active SDK session so the tool timeout and
* execution mode stay aligned.
* The terminal execution mode is captured when the session's tool set is
* built; switching modes rebuilds the active SDK session so the tool timeout
* and execution path follow it.
*
* The shell is snapshotted each time the runtime reads the tool description,
* which happens when a model request is built. Tool calls produced by that
* request execute with the same snapshot, so changing the terminal profile
* while the model is generating does not change the shell under commands the
* model has already planned: the new shell is named in the next request (the
* one carrying these tool results) and used by the commands it produces.
*/
export function createVscodeRunCommandsTool(options: VscodeRunCommandsToolOptions): AgentTool {
return createShellTool(createVscodeShellExecutor(options), {
const state = { snapshot: takeShellSnapshot() }
return createShellTool(createVscodeShellExecutor(options, state), {
cwd: options.cwd,
bashTimeoutMs: options.bashTimeoutMs,
shell: () => {
state.snapshot = takeShellSnapshot()
return state.snapshot.shell
},
})
}
function createVscodeShellExecutor(options: VscodeRunCommandsToolOptions): ShellExecutor {
function createVscodeShellExecutor(options: VscodeRunCommandsToolOptions, state: { snapshot: ShellSnapshot }): ShellExecutor {
const { cwd, getTerminalManager } = options
const executionMode = options.vscodeTerminalExecutionMode ?? "backgroundExec"
// Lazy-init background executor — recreated when the user's shell profile changes.
// Lazy-init background executor — recreated when the snapshotted shell changes.
let bgExecutor: ShellExecutor | undefined
let bgExecutorShell: string | undefined
@@ -200,12 +349,12 @@ function createVscodeShellExecutor(options: VscodeRunCommandsToolOptions): Shell
return async (command, commandCwd, context): Promise<string> => {
Logger.log(`[VscodeRunCommands] Executing command in ${executionMode} mode`)
if (executionMode === "backgroundExec") {
// Background path — use SDK's createShellExecutor
// Resolve shell from the user's terminal profile setting
const profileId = (StateManager.get().getGlobalSettingsKey("defaultTerminalProfile") as string) || "default"
const shell = getShellForProfile(profileId)
// Execute with the shell named in the model request that produced this
// tool call, not the setting's current value (see createVscodeRunCommandsTool).
const { profileId, shell } = state.snapshot
if (executionMode === "backgroundExec") {
// Background path — use SDK's createShellExecutor.
// Recreate the executor if the shell has changed
if (!bgExecutor || bgExecutorShell !== shell) {
bgExecutorShell = shell
@@ -240,6 +389,14 @@ function createVscodeShellExecutor(options: VscodeRunCommandsToolOptions): Shell
if (!terminalManager) {
terminalManager = getTerminalManager()
}
return await executeForeground(command, commandCwd || cwd, terminalManager, MAX_COMMAND_OUTPUT_CHARS, context.signal)
return await executeForeground(
command,
commandCwd || cwd,
terminalManager,
MAX_COMMAND_OUTPUT_CHARS,
context.signal,
options.foregroundCommands,
profileId,
)
}
}
@@ -3,6 +3,7 @@ import { type AgentTool, type AgentToolContext, createTool } from "@cline/shared
import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTerminalManager"
import type { McpHub } from "@/services/mcp/McpHub"
import { Logger } from "@/shared/services/Logger"
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
import { createVscodeRunCommandsTool, VSCODE_FOREGROUND_RUN_COMMANDS_TIMEOUT_MS } from "./vscode-run-commands-tool"
interface McpToolDescriptor {
@@ -124,6 +125,8 @@ export interface VscodeExtraToolsOptions {
getTerminalManager?: () => VscodeTerminalManager
/** Current VS Code terminal execution mode, captured when the session tools are built. */
vscodeTerminalExecutionMode?: "vscodeTerminal" | "backgroundExec"
/** Registry of in-flight foreground executions for "Proceed While Running". */
foregroundCommands?: SdkForegroundCommandCoordinator
}
export async function createVscodeExtraTools(mcpHub: McpHub, options?: VscodeExtraToolsOptions): Promise<AgentTool[]> {
@@ -159,6 +162,7 @@ export async function createVscodeExtraTools(mcpHub: McpHub, options?: VscodeExt
getTerminalManager: options.getTerminalManager,
bashTimeoutMs: executionMode === "vscodeTerminal" ? VSCODE_FOREGROUND_RUN_COMMANDS_TIMEOUT_MS : undefined,
vscodeTerminalExecutionMode: executionMode,
foregroundCommands: options.foregroundCommands,
}),
)
Logger.log(
+5 -1
View File
@@ -36,9 +36,10 @@ import type { VscodeTerminalManager } from "@/hosts/vscode/terminal/VscodeTermin
import { getDistinctId } from "@/services/logging/distinctId"
import type { McpHub } from "@/services/mcp/McpHub"
import { Logger } from "@/shared/services/Logger"
import type { SdkForegroundCommandCoordinator } from "./sdk-foreground-command-coordinator"
import type { SdkSessionHost } from "./session-host"
import { getEffectiveTerminalExecutionMode } from "./vscode-terminal-execution-mode"
import { createVscodeExtraTools } from "./vscode-runtime-builder"
import { getEffectiveTerminalExecutionMode } from "./vscode-terminal-execution-mode"
export interface VscodeSessionHostOptions {
mcpHub: McpHub
@@ -75,6 +76,8 @@ export interface VscodeSessionHostOptions {
* with a custom tool that supports foreground/background terminal execution.
*/
getTerminalManager?: () => VscodeTerminalManager
/** Registry of in-flight foreground executions for "Proceed While Running". */
foregroundCommands?: SdkForegroundCommandCoordinator
}
export class VscodeSessionHost implements SdkSessionHost {
@@ -133,6 +136,7 @@ export class VscodeSessionHost implements SdkSessionHost {
cwd: inputWithRemoteConfig.config.cwd,
getTerminalManager: options.getTerminalManager,
vscodeTerminalExecutionMode: getEffectiveTerminalExecutionMode(requestedTerminalExecutionMode),
foregroundCommands: options.foregroundCommands,
})
return {
...inputWithRemoteConfig,
@@ -124,6 +124,7 @@ export class WebviewGrpcBridge {
stateManager,
mcpHub: undefined,
backgroundCommandRunning: false,
foregroundCommandRunning: false,
backgroundCommandTaskId: undefined,
})
await sendStateUpdate(state)
@@ -93,6 +93,11 @@ export interface ExtensionState {
vscodeTerminalExecutionMode: string
backgroundCommandRunning?: boolean
backgroundCommandTaskId?: string
/**
* True while a foreground (VS Code terminal) command is awaited by a
* run_commands tool call. Drives the "Proceed While Running" button.
*/
foregroundCommandRunning?: boolean
lastCompletedCommandTs?: number
userInfo?: UserInfo
version: string
+4 -2
View File
@@ -61,8 +61,10 @@
* JetBrains exports trusted certificates from the OS and writes them to a
* temporary file, then configures node TLS by setting NODE_EXTRA_CA_CERTS.
*
* CLI users should set the NODE_EXTRA_CA_CERTS environment variable if
* necessary, because node does not automatically use the OS' trusted certs.
* The CLI's npm wrapper (bin/cline) does the same automatically: it harvests
* the OS trust store and points the child's NODE_EXTRA_CA_CERTS at a managed
* bundle, because the Bun runtime does not read the OS store on its own. A
* user-set NODE_EXTRA_CA_CERTS is merged in rather than replaced.
*
* ## Limitations in JetBrains & CLI
*
@@ -101,6 +101,10 @@ export function createShellExecutor() {
return async () => ""
}
// The real createShellTool, so tests exercise the actual description
// building and shell classification (getShellKind) rather than a stub that
// would have to duplicate those invariants.
export { createShellTool } from "../../../../sdk/packages/core/src/extensions/tools/definitions"
// Real (dependency-light) edit-executor implementations, re-exported from the sdk source so
// the diff-edit coordinator and its tests exercise the actual content/parse semantics. These
// modules only pull in node:fs/node:path and the patch parser — not the heavy core runtime.
@@ -114,13 +118,6 @@ export { createEditorExecutor } from "../../../../sdk/packages/core/src/extensio
export type { EditFileInput } from "../../../../sdk/packages/core/src/extensions/tools/schemas"
export type { ApplyPatchExecutor, EditorExecutor } from "../../../../sdk/packages/core/src/extensions/tools/types"
export function createShellTool(execute: unknown) {
return {
name: "run_commands",
execute,
}
}
export interface SessionHistoryRecord {
id: string
metadata?: Record<string, unknown>
+128 -5
View File
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
import { expect } from "chai"
import * as actualFs from "fs"
import * as actualOs from "os"
import * as vscode from "vscode"
@@ -15,6 +16,16 @@ const osMock = () => ({ ...osMockNamespace, default: osMockNamespace })
mock.module("os", osMock)
mock.module("node:os", osMock)
// getShell() probes the filesystem for PowerShell 7 when no Windows terminal
// profile is configured. Route existsSync through a mutable delegate so tests
// control which PowerShell installs "exist" regardless of the host machine.
let existsSyncImpl: typeof actualFs.existsSync = actualFs.existsSync
const existsSyncDelegate = ((path: unknown) => existsSyncImpl(path as string)) as typeof actualFs.existsSync
const fsMockNamespace = { ...actualFs, existsSync: existsSyncDelegate }
const fsMock = () => ({ ...fsMockNamespace, default: fsMockNamespace })
mock.module("fs", fsMock)
mock.module("node:fs", fsMock)
import { getShell } from "@utils/shell"
describe("Shell Detection Tests", () => {
@@ -22,6 +33,7 @@ describe("Shell Detection Tests", () => {
let originalEnv: NodeJS.ProcessEnv
let originalGetConfig: typeof vscode.workspace.getConfiguration
let originalUserInfo: typeof actualOs.userInfo
let originalExistsSync: typeof actualFs.existsSync
// Helper to mock VS Code configuration
function mockVsCodeConfig(platformKey: string, defaultProfileName: string | null, profiles: Record<string, any>) {
@@ -45,6 +57,7 @@ describe("Shell Detection Tests", () => {
originalEnv = { ...process.env }
originalGetConfig = vscode.workspace.getConfiguration
originalUserInfo = userInfoImpl
originalExistsSync = existsSyncImpl
// Clear environment variables for a clean test
delete process.env.SHELL
@@ -52,6 +65,9 @@ describe("Shell Detection Tests", () => {
// Default userInfo() mock
userInfoImpl = (() => ({ shell: null })) as any
// Default: PowerShell 7 is not installed, so the Windows default
// resolves to legacy Windows PowerShell.
existsSyncImpl = (() => false) as any
})
afterEach(() => {
@@ -60,6 +76,7 @@ describe("Shell Detection Tests", () => {
process.env = originalEnv
vscode.workspace.getConfiguration = originalGetConfig
userInfoImpl = originalUserInfo
existsSyncImpl = originalExistsSync
})
// --------------------------------------------------------------------------
@@ -71,12 +88,63 @@ describe("Shell Detection Tests", () => {
})
it("uses explicit PowerShell 7 path from VS Code config (profile path)", () => {
existsSyncImpl = ((candidate: actualFs.PathLike) =>
candidate === "C:\\Program Files\\PowerShell\\7\\pwsh.exe") as typeof actualFs.existsSync
mockVsCodeConfig("windows", "PowerShell", {
PowerShell: { path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe" },
})
expect(getShell()).to.equal("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
})
it("expands and selects the first configured profile path when it exists", () => {
process.env.windir = "C:\\Windows"
existsSyncImpl = (() => true) as typeof actualFs.existsSync
mockVsCodeConfig("windows", "Command Prompt", {
"Command Prompt": {
path: [`\${env:windir}\\Sysnative\\cmd.exe`, `\${env:windir}\\System32\\cmd.exe`],
},
})
expect(getShell()).to.equal("C:\\Windows\\Sysnative\\cmd.exe")
})
it("falls through configured profile paths in order", () => {
process.env.windir = "C:\\Windows"
existsSyncImpl = ((candidate: actualFs.PathLike) =>
candidate === "C:\\Windows\\System32\\cmd.exe") as typeof actualFs.existsSync
mockVsCodeConfig("windows", "Command Prompt", {
"Command Prompt": {
path: [`\${env:windir}\\Sysnative\\cmd.exe`, `\${env:windir}\\System32\\cmd.exe`],
},
})
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
})
it("skips profile paths with variable references it cannot expand", () => {
existsSyncImpl = ((candidate: actualFs.PathLike) =>
candidate === "C:\\Windows\\System32\\cmd.exe") as typeof actualFs.existsSync
process.env.windir = "C:\\Windows"
mockVsCodeConfig("windows", "Command Prompt", {
"Command Prompt": {
path: [`\${workspaceFolder}\\tools\\cmd.exe`, `\${env:windir}\\System32\\cmd.exe`],
},
})
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
})
it("resolves a configured executable name from PATH", () => {
process.env.PATH = "C:\\Tools;C:\\Windows\\System32"
existsSyncImpl = ((candidate: actualFs.PathLike) =>
candidate === "C:\\Windows\\System32\\cmd.exe") as typeof actualFs.existsSync
mockVsCodeConfig("windows", "Command Prompt", {
"Command Prompt": { path: "cmd.exe" },
})
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
})
it("uses PowerShell 7 path if source is 'PowerShell' but no explicit path", () => {
mockVsCodeConfig("windows", "PowerShell", {
PowerShell: { source: "PowerShell" },
@@ -117,18 +185,36 @@ describe("Shell Detection Tests", () => {
expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe")
})
it("respects userInfo() if no VS Code config is available", () => {
it("defaults to PowerShell 7 when no profile is configured and pwsh is installed", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
userInfoImpl = () => ({ shell: "C:\\Custom\\PowerShell.exe" }) as any
process.env.ProgramW6432 = "C:\\Program Files"
existsSyncImpl = (() => true) as any
expect(getShell()).to.equal("C:\\Custom\\PowerShell.exe")
expect(getShell()).to.equal("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
})
it("respects an odd COMSPEC if no userInfo shell is available", () => {
it("defaults to Store-installed pwsh when that is the only pwsh present", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
process.env.LOCALAPPDATA = "C:\\Users\\Test\\AppData\\Local"
const storePwsh = "C:\\Users\\Test\\AppData\\Local\\Microsoft\\WindowsApps\\pwsh.exe"
existsSyncImpl = ((path: string) => path === storePwsh) as any
expect(getShell()).to.equal(storePwsh)
})
it("defaults to legacy Windows PowerShell when no profile is configured and pwsh is absent", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
existsSyncImpl = (() => false) as any
expect(getShell()).to.equal("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
})
it("ignores userInfo() and COMSPEC — VS Code's default terminal ignores them too", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
userInfoImpl = () => ({ shell: "C:\\Custom\\OtherShell.exe" }) as any
process.env.COMSPEC = "D:\\CustomCmd\\cmd.exe"
expect(getShell()).to.equal("D:\\CustomCmd\\cmd.exe")
expect(getShell()).to.equal("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
})
})
@@ -141,12 +227,31 @@ describe("Shell Detection Tests", () => {
})
it("uses VS Code profile path if available", () => {
existsSyncImpl = ((candidate: actualFs.PathLike) => candidate === "/usr/local/bin/fish") as typeof actualFs.existsSync
mockVsCodeConfig("osx", "MyCustomShell", {
MyCustomShell: { path: "/usr/local/bin/fish" },
})
expect(getShell()).to.equal("/usr/local/bin/fish")
})
it("expands and selects the first existing path in an array-valued profile", () => {
existsSyncImpl = ((candidate: actualFs.PathLike) => candidate === "/bin/zsh") as typeof actualFs.existsSync
mockVsCodeConfig("osx", "MyCustomShell", {
MyCustomShell: { path: ["/opt/homebrew/bin/zsh", "/bin/zsh"] },
})
expect(getShell()).to.equal("/bin/zsh")
})
it("falls back past a configured path that does not exist", () => {
existsSyncImpl = (() => false) as typeof actualFs.existsSync
mockVsCodeConfig("osx", "MyCustomShell", {
MyCustomShell: { path: "/missing/shell" },
})
userInfoImpl = () => ({ shell: "/opt/homebrew/bin/zsh" }) as any
expect(getShell()).to.equal("/opt/homebrew/bin/zsh")
})
it("falls back to userInfo().shell if no VS Code config is available", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
userInfoImpl = () => ({ shell: "/opt/homebrew/bin/zsh" }) as any
@@ -177,12 +282,30 @@ describe("Shell Detection Tests", () => {
})
it("uses VS Code profile path if available", () => {
existsSyncImpl = ((candidate: actualFs.PathLike) => candidate === "/usr/bin/fish") as typeof actualFs.existsSync
mockVsCodeConfig("linux", "CustomProfile", {
CustomProfile: { path: "/usr/bin/fish" },
})
expect(getShell()).to.equal("/usr/bin/fish")
})
it("expands and selects the first existing path in an array-valued profile", () => {
existsSyncImpl = ((candidate: actualFs.PathLike) => candidate === "/bin/bash") as typeof actualFs.existsSync
mockVsCodeConfig("linux", "CustomProfile", {
CustomProfile: { path: ["/usr/bin/fish", "/bin/bash"] },
})
expect(getShell()).to.equal("/bin/bash")
})
it("resolves a bare executable name from PATH without PATHEXT probing", () => {
process.env.PATH = "/opt/tools:/usr/bin"
existsSyncImpl = ((candidate: actualFs.PathLike) => candidate === "/usr/bin/fish") as typeof actualFs.existsSync
mockVsCodeConfig("linux", "CustomProfile", {
CustomProfile: { path: "fish" },
})
expect(getShell()).to.equal("/usr/bin/fish")
})
it("falls back to userInfo().shell if no VS Code config is available", () => {
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
userInfoImpl = () => ({ shell: "/usr/bin/zsh" }) as any
+2 -9
View File
@@ -1,6 +1,6 @@
import * as childProcess from "child_process"
import { Logger } from "@/shared/services/Logger"
import { WINDOWS_POWERSHELL_7_PATH, WINDOWS_POWERSHELL_LEGACY_PATH } from "./shell"
import { getWindowsPwshInstallPaths, WINDOWS_POWERSHELL_LEGACY_PATH } from "./shell"
const POWERSHELL_PROBE_TIMEOUT_MS = 1200
@@ -16,14 +16,7 @@ export function getFallbackWindowsPowerShellPath(): string {
}
export function getWindowsPowerShellCandidates(): string[] {
const programFiles = process.env.ProgramW6432 || process.env.ProgramFiles || "C:\\Program Files"
const envAbsoluteCandidates = [
`${programFiles}\\PowerShell\\7\\pwsh.exe`,
`${programFiles}\\PowerShell\\6\\pwsh.exe`,
WINDOWS_POWERSHELL_7_PATH,
WINDOWS_POWERSHELL_LEGACY_PATH,
]
const envAbsoluteCandidates = [...getWindowsPwshInstallPaths(), WINDOWS_POWERSHELL_LEGACY_PATH]
const commandNameFallbacks = ["pwsh.exe", "pwsh", "powershell.exe", "powershell"]
+129 -24
View File
@@ -1,5 +1,8 @@
import { existsSync } from "fs"
import { userInfo } from "os"
import * as nodePath from "path"
import * as vscode from "vscode"
import { Logger } from "@/shared/services/Logger"
export const WINDOWS_POWERSHELL_7_PATH = "C:\\Program Files\\PowerShell\\7\\pwsh.exe"
export const WINDOWS_POWERSHELL_LEGACY_PATH = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
@@ -29,21 +32,24 @@ const SHELL_PATHS = {
FALLBACK: "/bin/sh",
} as const
// VS Code permits `path: string | string[]` in terminal profiles on every
// platform (the stock Windows "Command Prompt" profile is an array), so all
// three profile shapes model both forms.
interface MacTerminalProfile {
path?: string
path?: string | string[]
}
type MacTerminalProfiles = Record<string, MacTerminalProfile>
interface WindowsTerminalProfile {
path?: string
path?: string | string[]
source?: "PowerShell" | "WSL"
}
type WindowsTerminalProfiles = Record<string, WindowsTerminalProfile>
interface LinuxTerminalProfile {
path?: string
path?: string | string[]
}
type LinuxTerminalProfiles = Record<string, LinuxTerminalProfile>
@@ -89,6 +95,83 @@ function getLinuxTerminalConfig() {
// 2) Platform-Specific VS Code Shell Retrieval
// -----------------------------------------------------
function isWindows(): boolean {
return process.platform === "win32"
}
/** The path module matching the host platform's separators and semantics. */
function hostPath(): nodePath.PlatformPath {
return isWindows() ? nodePath.win32 : nodePath.posix
}
function getEnvironmentVariable(name: string): string | undefined {
// Windows environment variable names are case-insensitive; POSIX names
// are case-sensitive.
if (!isWindows()) {
return process.env[name]
}
const entry = Object.entries(process.env).find(([key]) => key.toLowerCase() === name.toLowerCase())
return entry?.[1]
}
/** Expands VS Code's `${env:NAME}` references in a profile path. */
function expandShellPath(candidate: string): string {
return candidate.replace(/\$\{env:([^}]+)\}/gi, (reference, name: string) => {
return getEnvironmentVariable(name.trim()) ?? reference
})
}
/**
* Resolves a profile path candidate to an existing executable: absolute or
* relative paths are checked directly; bare names are searched on PATH
* (with PATHEXT on Windows, mirroring how VS Code launches profiles).
*/
function findExecutable(candidate: string): string | null {
const path = hostPath()
if (path.basename(candidate) !== candidate) {
const normalized = path.normalize(candidate)
return existsSync(normalized) ? normalized : null
}
const pathValue = getEnvironmentVariable("PATH")
if (!pathValue) {
return null
}
const extensions =
!isWindows() || path.extname(candidate) ? [""] : (getEnvironmentVariable("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";")
for (const directory of pathValue.split(path.delimiter)) {
for (const extension of extensions) {
const executable = path.join(directory, `${candidate}${extension}`)
if (existsSync(executable)) {
return executable
}
}
}
return null
}
/** Resolves the first usable path in a VS Code terminal profile. */
function resolveShellPath(configuredPath: string | string[] | undefined): string | null {
const candidates = typeof configuredPath === "string" ? [configuredPath] : configuredPath
for (const candidate of candidates ?? []) {
const expandedPath = expandShellPath(candidate)
if (expandedPath.includes("${")) {
// Only ${env:NAME} references are expanded here. VS Code resolves
// more variable kinds (e.g. ${workspaceFolder}); surface the gap
// instead of silently skipping the user's configured shell.
Logger.warn(`[shell] Skipping terminal profile path with unresolved variable reference: ${candidate}`)
continue
}
const executable = findExecutable(expandedPath)
if (executable) {
return executable
}
}
return null
}
/** Attempts to retrieve a shell path from VS Code config on Windows. */
function getWindowsShellFromVSCode(): string | null {
const { defaultProfileName, profiles } = getWindowsTerminalConfig()
@@ -97,14 +180,15 @@ function getWindowsShellFromVSCode(): string | null {
}
const profile = profiles[defaultProfileName]
const configuredShell = resolveShellPath(profile?.path)
// If the profile name indicates PowerShell, do version-based detection.
// In testing it was found these typically do not have a path, and this
// implementation manages to deductively get the correct version of PowerShell
if (defaultProfileName.toLowerCase().includes("powershell")) {
if (profile?.path) {
if (configuredShell) {
// If there's an explicit PowerShell path, return that
return profile.path
return configuredShell
}
if (profile?.source === "PowerShell") {
// If the profile is sourced from PowerShell, assume the newest
@@ -115,8 +199,8 @@ function getWindowsShellFromVSCode(): string | null {
}
// If there's a specific path, return that immediately
if (profile?.path) {
return profile.path
if (configuredShell) {
return configuredShell
}
// If the profile indicates WSL
@@ -135,8 +219,7 @@ function getMacShellFromVSCode(): string | null {
return null
}
const profile = profiles[defaultProfileName]
return profile?.path || null
return resolveShellPath(profiles[defaultProfileName]?.path)
}
/** Attempts to retrieve a shell path from VS Code config on Linux. */
@@ -146,8 +229,7 @@ function getLinuxShellFromVSCode(): string | null {
return null
}
const profile = profiles[defaultProfileName]
return profile?.path || null
return resolveShellPath(profiles[defaultProfileName]?.path)
}
// -----------------------------------------------------
@@ -171,11 +253,6 @@ function getShellFromUserInfo(): string | null {
function getShellFromEnv(): string | null {
const { env } = process
if (process.platform === "win32") {
// On Windows, COMSPEC typically holds cmd.exe
return env.COMSPEC || "C:\\Windows\\System32\\cmd.exe"
}
if (process.platform === "darwin") {
// On macOS/Linux, SHELL is commonly the environment variable
return env.SHELL || "/bin/zsh"
@@ -304,6 +381,35 @@ export function getShellForProfile(profileId: string): string {
// 5) Publicly Exposed Shell Getter
// -----------------------------------------------------
/**
* Absolute paths where a modern PowerShell (pwsh) may be installed, most
* preferred first: MSI/ZIP installs under Program Files (either architecture),
* then the Microsoft Store install under LOCALAPPDATA. This is the single
* candidate list shared with the async prober in utils/powershell.ts.
*/
export function getWindowsPwshInstallPaths(): string[] {
const programFiles = process.env.ProgramW6432 || process.env.ProgramFiles || "C:\\Program Files"
const localAppData = process.env.LOCALAPPDATA
return [
`${programFiles}\\PowerShell\\7\\pwsh.exe`,
`${programFiles}\\PowerShell\\6\\pwsh.exe`,
SHELL_PATHS.POWERSHELL_7,
...(localAppData ? [`${localAppData}\\Microsoft\\WindowsApps\\pwsh.exe`] : []),
]
}
/**
* The shell VS Code launches on Windows when the user has not configured a
* default terminal profile: its built-in default is PowerShell (pwsh when
* installed, Windows PowerShell otherwise) never cmd.exe. Mirroring that
* here keeps the "default" profile meaning the same shell whether commands
* run in a visible VS Code terminal or a background child process.
*/
function getWindowsDefaultShell(): string {
const pwsh = getWindowsPwshInstallPaths().find((candidate) => existsSync(candidate))
return pwsh ?? SHELL_PATHS.POWERSHELL_LEGACY
}
export function getShell(): string {
// 1. Check VS Code config first.
if (process.platform === "win32") {
@@ -312,7 +418,12 @@ export function getShell(): string {
if (windowsShell) {
return windowsShell
}
} else if (process.platform === "darwin") {
// No profile configured — match the shell VS Code's default terminal
// would launch. userInfo()/COMSPEC are not consulted: VS Code's own
// terminal ignores them too, and they would resolve to cmd.exe.
return getWindowsDefaultShell()
}
if (process.platform === "darwin") {
// macOS from VS Code
const macShell = getMacShellFromVSCode()
if (macShell) {
@@ -338,12 +449,6 @@ export function getShell(): string {
return envShell
}
// 4. Finally, fall back to a default
if (process.platform === "win32") {
// On Windows, if we got here, we have no config, no COMSPEC, and one very messed up operating system.
// Use CMD as a last resort
return SHELL_PATHS.CMD
}
// On macOS/Linux, fallback to a POSIX shell - This is the behavior of our old shell detection method.
// 4. Fall back to a POSIX shell - This is the behavior of our old shell detection method.
return SHELL_PATHS.FALLBACK
}
@@ -20,7 +20,7 @@ interface ActionButtonsProps {
*/
export const ActionButtons: React.FC<ActionButtonsProps> = ({ task, messages, chatState, mode, messageHandlers }) => {
const { inputValue, selectedImages, selectedFiles, setSendingDisabled } = chatState
const { turnState } = useExtensionState()
const { turnState, foregroundCommandRunning } = useExtensionState()
// Tracks the ask the user last acted on. Clicking a footer button latches this so the
// buttons disable immediately (and survive the trailing bookkeeping re-renders before the
@@ -41,8 +41,8 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({ task, messages, ch
// buttons immune to trailing bookkeeping messages and never disagree with the thinking
// indicator (RC1).
const buttonConfig = useMemo(() => {
return getButtonConfigFromState(messages, turnState, mode)
}, [messages, turnState, mode])
return getButtonConfigFromState(messages, turnState, mode, foregroundCommandRunning)
}, [messages, turnState, mode, foregroundCommandRunning])
// Identity of the ask that currently owns the footer buttons. The button config objects are
// shared singletons (e.g. BUTTON_CONFIGS.tool_approve), so two consecutive identical asks
@@ -367,6 +367,15 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
clearInputState()
break
case "proceed_while_running":
// Detach the running foreground terminal command: the agent
// receives the partial output plus a log file path for the
// rest, and the command keeps running in the terminal.
await TaskServiceClient.proceedWhileRunningCommand(EmptyRequest.create({})).catch((err) =>
console.error("Failed to proceed while running:", err),
)
break
case "new_task":
if (clineAsk === "new_task") {
await TaskServiceClient.newTask(
@@ -252,4 +252,19 @@ describe("getButtonConfigFromState (dispatch + legacy fallback)", () => {
const turnState: TurnState = { phase: "completed", seq: 3 }
expect(getButtonConfigFromState(messages, turnState, "act")).toEqual(BUTTON_CONFIGS.completion_result)
})
it("streaming phase shows Proceed While Running when a foreground command is running", () => {
const messages: ClineMessage[] = [{ ts: 1, type: "say", say: "command", text: "npm run dev", partial: true }]
const turnState: TurnState = { phase: "streaming", seq: 4 }
expect(getButtonConfigFromState(messages, turnState, "act", true)).toEqual(BUTTON_CONFIGS.foreground_command_running)
expect(getButtonConfigFromState(messages, turnState, "act", false)).toEqual(BUTTON_CONFIGS.partial)
})
it("foreground command flag only affects the streaming phase", () => {
const messages: ClineMessage[] = []
expect(getButtonConfigFromState(messages, { phase: "completed", seq: 5 }, "act", true)).toEqual(
BUTTON_CONFIGS.completion_result,
)
expect(getButtonConfigFromState(messages, { phase: "idle", seq: 6 }, "act", true)).toEqual(BUTTON_CONFIGS.default)
})
})
@@ -8,6 +8,7 @@ export type ButtonActionType =
| "approve" // Send yesButtonClicked
| "reject" // Send noButtonClicked
| "proceed" // Send messageResponse or yesButtonClicked
| "proceed_while_running" // Detach the running foreground terminal command
| "new_task" // Start a new task
| "cancel" // Cancel streaming
| "utility" // Execute utility function (condense, report_bug)
@@ -188,6 +189,17 @@ export const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
secondaryAction: "cancel",
},
// A foreground terminal command is running (SDK path): the user can detach
// it and let the agent proceed with the partial output, or cancel the task.
foreground_command_running: {
sendingDisabled: true,
enableButtons: true,
primaryText: "Proceed While Running",
secondaryText: "Cancel",
primaryAction: "proceed_while_running",
secondaryAction: "cancel",
},
// Default states
default: {
sendingDisabled: false,
@@ -360,12 +372,19 @@ export function getButtonConfigForMessages(messages: ClineMessage[], mode: Mode
* The button SET is chosen by phase; the LABEL/variant for approvals (Save vs Approve, command
* vs tool vs MCP vs subagents) comes from the anchored message (turnState.anchorTs).
*/
export function buttonsForPhase(turnState: TurnState, anchoredMessage: ClineMessage | undefined): ButtonConfig {
export function buttonsForPhase(
turnState: TurnState,
anchoredMessage: ClineMessage | undefined,
foregroundCommandRunning = false,
): ButtonConfig {
switch (turnState.phase) {
case "idle":
return BUTTON_CONFIGS.default
case "streaming":
return BUTTON_CONFIGS.partial
// A running foreground terminal command offers "Proceed While Running":
// detach the command (output continues to a log file) and let the
// agent continue with the partial output.
return foregroundCommandRunning ? BUTTON_CONFIGS.foreground_command_running : BUTTON_CONFIGS.partial
case "completed":
return BUTTON_CONFIGS.completion_result
case "resumable":
@@ -398,10 +417,11 @@ export function getButtonConfigFromState(
messages: ClineMessage[],
turnState: TurnState | undefined,
mode: Mode = "act",
foregroundCommandRunning = false,
): ButtonConfig {
if (turnState) {
const anchored = turnState.anchorTs !== undefined ? messages.find((m) => m.ts === turnState.anchorTs) : undefined
return buttonsForPhase(turnState, anchored)
return buttonsForPhase(turnState, anchored, foregroundCommandRunning)
}
return getButtonConfigForMessages(messages, mode)
}
@@ -510,7 +510,7 @@ const ApiOptions = ({
<AIhubmixProvider currentMode={currentMode} isPopup={isPopup} showModelOptions={showModelOptions} />
)}
{apiConfiguration && (selectedProvider.includes("openai") || isCustomProvider) && (
{apiConfiguration && (selectedProvider === "openai" || isCustomProvider) && (
<OpenAICompatibleProvider
currentMode={currentMode}
isPopup={isPopup}
@@ -97,6 +97,25 @@ describe("ApiOptions Component", () => {
expect(modelIdInput).toBeInTheDocument()
})
it.each([
["openai-native", "OpenAI API Key"],
["openai-codex", "Sign in to OpenAI Codex"],
])("renders only the dedicated form for %s", (provider, dedicatedFormText) => {
mockExtensionState({
planModeApiProvider: provider as any,
actModeApiProvider: provider as any,
})
render(
<ExtensionStateContextProvider>
<ApiOptions currentMode="plan" showModelOptions={false} />
</ExtensionStateContextProvider>,
)
expect(screen.getByText(dedicatedFormText)).toBeInTheDocument()
expect(screen.queryByText("Custom Headers")).not.toBeInTheDocument()
})
it("renders the OpenAI-compatible form for custom/unknown catalog providers", () => {
vi.mocked(useProviderListings).mockReturnValue({
providers: [
@@ -0,0 +1,84 @@
import type { AuthState, UserOrganizationsResponse } from "@shared/proto/cline/account"
import { act, render, screen } from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { ClineAuthProvider, useClineAuth } from "./ClineAuthContext"
type AuthStatusCallbacks = {
onResponse: (response: AuthState) => void
}
const grpcMocks = vi.hoisted(() => ({
getUserOrganizations: vi.fn(),
subscribeToAuthStatusUpdate: vi.fn(),
authStatusCallbacks: undefined as AuthStatusCallbacks | undefined,
}))
vi.mock("@/services/grpc-client", () => ({
AccountServiceClient: {
getUserOrganizations: grpcMocks.getUserOrganizations,
subscribeToAuthStatusUpdate: grpcMocks.subscribeToAuthStatusUpdate,
},
}))
function createDeferred<T>() {
let resolve: (value: T) => void = () => {}
const promise = new Promise<T>((promiseResolve) => {
resolve = promiseResolve
})
return { promise, resolve }
}
function AuthStateProbe() {
const { clineUser, organizations } = useClineAuth()
return (
<>
<div data-testid="user-state">{clineUser?.uid ?? "signed-out"}</div>
<div data-testid="organizations-state">
{organizations?.map((organization) => organization.organizationId).join(",") ?? "none"}
</div>
</>
)
}
describe("ClineAuthProvider", () => {
beforeEach(() => {
vi.clearAllMocks()
grpcMocks.authStatusCallbacks = undefined
grpcMocks.subscribeToAuthStatusUpdate.mockImplementation((_request, callbacks: AuthStatusCallbacks) => {
grpcMocks.authStatusCallbacks = callbacks
return vi.fn()
})
})
it("does not restore organizations when an in-flight request resolves after sign-out", async () => {
const organizationsRequest = createDeferred<UserOrganizationsResponse>()
grpcMocks.getUserOrganizations.mockReturnValue(organizationsRequest.promise)
render(
<ClineAuthProvider>
<AuthStateProbe />
</ClineAuthProvider>,
)
act(() => {
grpcMocks.authStatusCallbacks?.onResponse({ user: { uid: "user-1" } })
})
expect(grpcMocks.getUserOrganizations).toHaveBeenCalledTimes(1)
act(() => {
grpcMocks.authStatusCallbacks?.onResponse({})
})
await act(async () => {
organizationsRequest.resolve({
organizations: [
{ organizationId: "stale-org", active: true, memberId: "member-1", name: "Stale Org", roles: [] },
],
})
await organizationsRequest.promise
})
expect(screen.getByTestId("user-state")).toHaveTextContent("signed-out")
expect(screen.getByTestId("organizations-state")).toHaveTextContent("none")
})
})
@@ -1,8 +1,8 @@
import type { UserOrganization } from "@shared/proto/cline/account"
import type { AuthState, UserOrganization } from "@shared/proto/cline/account"
import { EmptyRequest } from "@shared/proto/cline/common"
import deepEqual from "fast-deep-equal"
import type React from "react"
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
import { AccountServiceClient } from "@/services/grpc-client"
// Define User type (you may need to adjust this based on your actual User type)
@@ -25,10 +25,15 @@ export const ClineAuthContext = createContext<ClineAuthContextType | undefined>(
export const ClineAuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<ClineUser | null>(null)
const [userOrganizations, setUserOrganizations] = useState<UserOrganization[] | null>(null)
const organizationsRequestIdRef = useRef(0)
const getUserOrganizations = useCallback(async () => {
const requestId = ++organizationsRequestIdRef.current
try {
const response = await AccountServiceClient.getUserOrganizations(EmptyRequest.create())
if (requestId !== organizationsRequestIdRef.current) {
return
}
setUserOrganizations((old) => {
if (!deepEqual(response.organizations, old)) {
return response.organizations
@@ -52,22 +57,23 @@ export const ClineAuthProvider: React.FC<{ children: React.ReactNode }> = ({ chi
// Handle auth status update events
useEffect(() => {
const cancelSubscription = AccountServiceClient.subscribeToAuthStatusUpdate(EmptyRequest.create(), {
onResponse: async (response: any) => {
setUser((oldUser) => {
if (!response?.user?.uid) {
return null
}
onResponse: (response: AuthState) => {
const responseUser = response.user
if (!responseUser?.uid) {
organizationsRequestIdRef.current++
setUser(null)
setUserOrganizations(null)
return
}
if (response?.user && oldUser?.uid !== response.user.uid) {
// Once we have a new user, fetch organizations that
// allow us to display the active account in account view UI
// and fetch the correct credit balance to display on mount
getUserOrganizations()
return response.user
}
// Refresh organizations on every auth status update, not just user
// changes. Switching organizations doesn't change the uid, so gating
// this on uid changes leaves stale `active` flags — which reset the
// account view's org dropdown on remount. The deepEqual guard in
// getUserOrganizations prevents no-op re-renders.
getUserOrganizations()
return oldUser
})
setUser((oldUser) => (oldUser?.uid !== responseUser.uid ? responseUser : oldUser))
},
onError: (error: Error) => {
console.error("Error in auth callback subscription:", error)
@@ -79,6 +85,7 @@ export const ClineAuthProvider: React.FC<{ children: React.ReactNode }> = ({ chi
// Cleanup function to cancel subscription when component unmounts
return () => {
organizationsRequestIdRef.current++
cancelSubscription()
}
}, [getUserOrganizations])
@@ -311,6 +311,7 @@ export const ExtensionStateContextProvider: React.FC<{
remoteConfigSettings: {},
backgroundCommandRunning: false,
backgroundCommandTaskId: undefined,
foregroundCommandRunning: false,
lastDismissedCliBannerVersion: 0,
backgroundEditEnabled: false,
showFeatureTips: true,
+119 -79
View File
@@ -19,7 +19,7 @@
},
"apps/cli": {
"name": "@cline/cli",
"version": "3.0.41",
"version": "3.0.45",
"bin": {
"cline": "src/index.ts",
},
@@ -632,7 +632,7 @@
},
"sdk/packages/agents": {
"name": "@cline/agents",
"version": "0.0.62",
"version": "0.0.65",
"dependencies": {
"@cline/llms": "workspace:*",
"@cline/shared": "workspace:*",
@@ -641,7 +641,7 @@
},
"sdk/packages/core": {
"name": "@cline/core",
"version": "0.0.62",
"version": "0.0.65",
"dependencies": {
"@cline/agents": "workspace:*",
"@cline/llms": "workspace:*",
@@ -679,7 +679,7 @@
},
"sdk/packages/llms": {
"name": "@cline/llms",
"version": "0.0.62",
"version": "0.0.65",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.89",
"@ai-sdk/anthropic": "^3.0.68",
@@ -698,30 +698,36 @@
"@streamparser/json": "^0.0.21",
"ai": "^6.0.144",
"ai-sdk-ollama": "^3.8.8",
"ai-sdk-provider-claude-code": "^3.4.3",
"ai-sdk-provider-codex-cli": "^1.1.0",
"ai-sdk-provider-opencode-sdk": "^3.0.1",
"dify-ai-provider": "^1.1.0",
"nanoid": "^5.1.7",
"zod": "^4.3.6",
},
"devDependencies": {
"ai-sdk-provider-claude-code": "^3.4.3",
"ai-sdk-provider-codex-cli": "^1.1.0",
},
"peerDependencies": {
"@aws-sdk/client-bedrock-runtime": "^3.0.0",
"ai-sdk-provider-claude-code": "^3.4.3",
"ai-sdk-provider-codex-cli": "^1.1.0",
},
"optionalPeers": [
"@aws-sdk/client-bedrock-runtime",
"ai-sdk-provider-claude-code",
"ai-sdk-provider-codex-cli",
],
},
"sdk/packages/sdk": {
"name": "@cline/sdk",
"version": "0.0.62",
"version": "0.0.65",
"dependencies": {
"@cline/core": "workspace:*",
},
},
"sdk/packages/shared": {
"name": "@cline/shared",
"version": "0.0.62",
"version": "0.0.65",
"dependencies": {
"aws4fetch": "^1.0.20",
"jsonrepair": "^3.13.2",
@@ -731,11 +737,31 @@
},
"sdk/packages/ui": {
"name": "@cline/ui",
"version": "0.0.0",
"version": "0.1.0",
"devDependencies": {
"@fontsource-variable/schibsted-grotesk": "^5.2.8",
"@fontsource/azeret-mono": "^5.2.9",
"@storybook/addon-a11y": "^9.1.17",
"@storybook/addon-docs": "^9.1.17",
"@storybook/react-vite": "^9.1.6",
"@tailwindcss/vite": "^4.2.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"jsdom": "^26.0.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"storybook": "^9.1.17",
"tailwindcss": "^4.2.0",
"typescript": "5.9.3",
"vite": "^7.1.11",
"vitest": "^4.0.18",
},
"peerDependencies": {
"react": ">=18.3.0 <20",
"tailwindcss": ">=4.0.0 <5",
},
"optionalPeers": [
"react",
"tailwindcss",
],
},
@@ -768,27 +794,27 @@
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.131", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.95", "@ai-sdk/openai": "3.0.82", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-UrbM28zGFJV6xTn7wpv/uCsp/wMKb79MCuZC3Ff1a59PGjLr+iMN+Nlul7cWV04pjE5u5kLP5SybtGZtDP2EkQ=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.133", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.96", "@ai-sdk/openai": "3.0.84", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1WkPajjTkVvG3OZJPxZQ/ZWdIja71qybt9W5HHLF9ooTIlr0Ldo6QVOy31IiVO53iJEtUXu4rTEZlMJE3TzJA=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Q7NhioTX6m0hKni14Ip9EO6WedbIYcldQ/PsGB7gVAveRNog39FfX31f+9HYoEUrfm9L7QxIcB5aAzJV/hmNRg=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.96", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6VQzaXQdm5FkX6NWOyKzV5GB11C8IqkgsKZE91lg/bdwyvnQJLDwal2qkE0+fC8CCGeW5d+VV8Mw/+H+OcDC1A=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.145", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cqSQ+I0Bjj2W9g1oFyE1O1mSowsWXb+U1wK9vtg5kRQqB95iWVIKbtdr14gGf46cueJSiBlKfPTT24gDDVuFmw=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.148", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C/mTeSdmhu8dAnH3vZaRXffD8Oewo1r4QEGxvCdR8eC9b4PKPs2CLsbg3DVJH/X3Bea5fAu87Ob7P3fSS+oq/g=="],
"@ai-sdk/google": ["@ai-sdk/google@3.0.90", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nn2bSLFZDV5Xhl2oh+C3ckpBUM849zrHLoLe6B9DVu2DcgtIvxKYO0pKLO/vB9XQVKON5XORp7uV7P+60L5XUQ=="],
"@ai-sdk/google": ["@ai-sdk/google@3.0.91", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-d/ho+sDjArFjreE2002t9jE4LXX3wde97dN2HCLCX1l41gaJV3wf/c/19axjUNfIf/4uUq+nJmhBO0lW/dg3yw=="],
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.158", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.95", "@ai-sdk/google": "3.0.90", "@ai-sdk/openai-compatible": "2.0.58", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Z7KPZ2+M7DFnXPEDgElwazDQxDxYqd9HQdFLuCMSpc0No/1Dr0TKRT+Q5pJwstHJfIQeAMApFvalzGOfTe/ShQ=="],
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.159", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.96", "@ai-sdk/google": "3.0.91", "@ai-sdk/openai-compatible": "2.0.59", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AvHvV3Nw+LaLjTBveP96hBbKFXHoQBMeQa1fd9SV1UnXCvmCfl7cQempb+pZnho12rbTFHwgdX419nFTMNmATw=="],
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A/ov/CTrQ0rDztrvgYo9ql4u6tlyfTrwMN4u76zqLM0JqUWy82T82Y8HzP4fCQOs7gZggrruHaNbWnqnE9IwKA=="],
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SOZMjV48dyAn1rsiZSN7emeO0KYKnr9/SqMFPpJYUddPcnLSjac9GGWVKn+LnSzD7Woh3lYbYbJWdbJOQ2U2sQ=="],
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.82", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Gn1YliuNMneXoBmuLX1kH/e5SR/VnU9FXLvJ8WyiV61Noo+wPdE4nuzxRGt3lfV6rla1wyCb1syV4jU0A310ew=="],
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="],
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0SXA0xVt18F4ki7ttVshqaM0oLXSB475ACOU0/2RK3OZS3UYqrmKF+DJwYBYUZmqCq2nZ2vkNZEXpWP2wfGsrQ=="],
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.59", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CFsUizO+jL+jSlN13rW2nQE9EbWx+8PSFBdB3TtD0UGYOxtefCVa5hsrNo5NUOJJGX5f4xHiXE1i2m5nQ80QPg=="],
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.13", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw=="],
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VG4tpVXCuzm21U9xjg05BCMZnjZOazC72+MxBkLAa7hCKsnqNt542GYWUUqwmHSczJwgbSXN8UvaNgSerUaKdw=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
"@ai-sdk/react": ["@ai-sdk/react@3.0.223", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.37", "ai": "6.0.221", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-b7Ri+wLOR9pZkKlEKii3ZuXi79Rh3rC5rYUEFDpXUOJLazztIE4MPM4dQRXCojYgOuPa61gVO2BFjuRVLGsOfw=="],
"@ai-sdk/react": ["@ai-sdk/react@3.0.226", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.38", "ai": "6.0.224", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-pPLSwLlxpXzYypkIpDRWENtIB1aDvi1TSxXrt5UZjLX1e0uHNaTUZJiGa52M8G0sN00a2Q2+ASARqMZKaTZbXQ=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@@ -796,23 +822,23 @@
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.205", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.205", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.205", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.205", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.205", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.205", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.205", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.205", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.205" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-ft6iBw9kXudsusiXNpeybIPBJ07Z3tqp1ROSg5cEJqgA+9i+JJj2sRfQth+QD+lyenbbAU8yPieLxIimvfBhtw=="],
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.207", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.207", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.207", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.207", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.207", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.207", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.207", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.207", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.207" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-y0PkQRmQBi96MHiN5Xzfq+GaddxCZCqI/cXEQBLYBLXGa4i1nDSlulQqkMBj2RorrrSGQJ6Wdw+uhu6OfHNPzA=="],
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.205", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lrfJ4eVtzfPkCpbSkBOGSMQCBbvmW6nbPzgHE4IwMN3scZlpuFMUFqh2aaJa/X2SAcWD9H2S0t2WWvSRgM7BjA=="],
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.207", "", { "os": "darwin", "cpu": "arm64" }, "sha512-08xSo1FDx8h0aLhL5tvcRxa2SMmcUV3aDWeZiEJVTclyiDAs61BgTjAxCg+SZcu1CndjJO8cfO0yM5dhamxz3g=="],
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.205", "", { "os": "darwin", "cpu": "x64" }, "sha512-G6ETPmL5mNzJ2DFsWxG3jmsmrXgZX1N2ZCJvxaGUUpjTsKZJ4Tup1cWYvcd/m7o5fYZmx9REmgzTwsAIc1fdPQ=="],
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.207", "", { "os": "darwin", "cpu": "x64" }, "sha512-1o7K4EYqyCixZ/oeOZSh7AzSy6TM86xoOuf4VuORjPSS31hBnoqY0NGZd27+2VDs9LGtsdksmsTqcNGx9xd1hA=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.205", "", { "os": "linux", "cpu": "arm64" }, "sha512-CXzySK3PV3EizCRPXnxPqeaAtgrBFDnMFOVpMe36oC3U16yDb1b1tAJGqZi/7uFrVvAiaXvnSFxhUWnDDSaO+A=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.207", "", { "os": "linux", "cpu": "arm64" }, "sha512-X4uezYOifDiNTTmmugfRCdg3nNamrr1LFRY9hg30vWYTShL+bbN+nfC3KaFfSYCl4GTtsEEUbYdOTC2F3bBpcA=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.205", "", { "os": "linux", "cpu": "arm64" }, "sha512-91fgdG4aTnQ29sKOcUqgH4+tKCW2ut6PWGRSYmXNDbROasJm1rAlPdzC5brdu/e4c0CDSNV6TWyE5JCjaS/jlQ=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.207", "", { "os": "linux", "cpu": "arm64" }, "sha512-oPj+g2DslhH4Y9nCTs7al4t9wZv78FZwLFQwOCg99BXuz1o0ZOpKmxyvR7J9eBR+GPszeMMS8gYplQTiZC9o2w=="],
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.205", "", { "os": "linux", "cpu": "x64" }, "sha512-siS+1iNqBSlGFZZvJY6+mhzZ/6/ec/TbX9GMuwmTF0E6fxGhIIp797jJxR1q8r6FAq7d39mEoRNhC0Ffo60uNQ=="],
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.207", "", { "os": "linux", "cpu": "x64" }, "sha512-Kg6BPH8Ee0ny/oEUWJmvT1jCRBne4jVpRSOMsJcYp1Fav1rMEgpU219oJJs+LWwx4ifuuLtNWedqJNnVw7mnKg=="],
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.205", "", { "os": "linux", "cpu": "x64" }, "sha512-vvsb7GlnA8CTSVvvTkrXjcSeRKqxSM7p/tU3Od9ICAZeWHglptekEyzLEApzLuLbI5ewfFF/F0q3NwOBbo18dg=="],
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.207", "", { "os": "linux", "cpu": "x64" }, "sha512-uRv+D5oG/7EYr41FAJ9IPo2pZYBe2ZMaA6nSHCeizsgPxCSMtl5bNppmU21+jZJvo4hivObEgkGFERAhdGqygg=="],
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.205", "", { "os": "win32", "cpu": "arm64" }, "sha512-SpP5zF68weFez/6pKrGzq/UVAJDMDNphWqmkLfOpWTDBL5xy6XlIZw5Bl4EXoVnfi2VLFkwuffNeFe+9SdX7kw=="],
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.207", "", { "os": "win32", "cpu": "arm64" }, "sha512-9fWpUzfkXlPAg2tf8JpQe7w9avFaomAUbfAwyAmykQgSIf66LwaJjvI5hNqhNqczRKyfsXPn3ei2S5HKlmFP+Q=="],
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.205", "", { "os": "win32", "cpu": "x64" }, "sha512-kg2kkXyeSoFLruO3Ic2IruLxzBR0xCUtmlJHdWi3SYW7JhAKNJg4fcrdJsWcardmEw23Y2UDGDJbRyxqSVx6wg=="],
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.207", "", { "os": "win32", "cpu": "x64" }, "sha512-YPjVT0q6aXEM2MgN4CI6/9fqiTXwETji+4NoPOzCYuqAkhXZqp30Jsk7/NHqYGNNSfURKrsuAoliKB0rsbpbjg=="],
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.37.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw=="],
@@ -838,7 +864,7 @@
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.63", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ=="],
"@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1083.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/credential-provider-cognito-identity": "^3.972.56", "@aws-sdk/credential-provider-env": "^3.972.57", "@aws-sdk/credential-provider-http": "^3.972.59", "@aws-sdk/credential-provider-ini": "^3.973.1", "@aws-sdk/credential-provider-login": "^3.972.63", "@aws-sdk/credential-provider-node": "^3.972.66", "@aws-sdk/credential-provider-process": "^3.972.57", "@aws-sdk/credential-provider-sso": "^3.973.1", "@aws-sdk/credential-provider-web-identity": "^3.972.63", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-i2q3Jgt365lZp7BSDqDSf283WvISrXob1zsql093LK3G2svYRHRvcNv995SsKtAzRENwHem2TC2vWeghrgBkxg=="],
"@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1085.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/credential-provider-cognito-identity": "^3.972.56", "@aws-sdk/credential-provider-env": "^3.972.57", "@aws-sdk/credential-provider-http": "^3.972.59", "@aws-sdk/credential-provider-ini": "^3.973.1", "@aws-sdk/credential-provider-login": "^3.972.63", "@aws-sdk/credential-provider-node": "^3.972.66", "@aws-sdk/credential-provider-process": "^3.972.57", "@aws-sdk/credential-provider-sso": "^3.973.1", "@aws-sdk/credential-provider-web-identity": "^3.972.63", "@aws-sdk/nested-clients": "^3.997.31", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-t9bNvRalbNaU8lt/jA1LeFIwowf9ZDlBu/G2l8QnCVYs7yZ2muh/RYdMF0eT+PfgDto8yQ+ncrxFO0bi2uN6oA=="],
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.31", "", { "dependencies": { "@aws-sdk/core": "^3.975.1", "@aws-sdk/signature-v4-multi-region": "^3.996.39", "@aws-sdk/types": "^3.974.0", "@smithy/core": "^3.29.2", "@smithy/fetch-http-handler": "^5.6.4", "@smithy/node-http-handler": "^4.9.4", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw=="],
@@ -962,7 +988,7 @@
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
"@bufbuild/buf": ["@bufbuild/buf@1.71.0", "", { "optionalDependencies": { "@bufbuild/buf-darwin-arm64": "1.71.0", "@bufbuild/buf-darwin-x64": "1.71.0", "@bufbuild/buf-linux-aarch64": "1.71.0", "@bufbuild/buf-linux-armv7": "1.71.0", "@bufbuild/buf-linux-x64": "1.71.0", "@bufbuild/buf-win32-arm64": "1.71.0", "@bufbuild/buf-win32-x64": "1.71.0" }, "bin": { "buf": "bin/buf", "protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking", "protoc-gen-buf-lint": "bin/protoc-gen-buf-lint" } }, "sha512-GDcjBCwLgHT/4nX4YSnYatZ7sDZDpHV6dxQvoT2/P6gKvV23O6hl8NryzLIRKmeau0FRXpQKHVy1dMfnBSpy+w=="],
"@bufbuild/buf": ["@bufbuild/buf@1.71.0", "", { "optionalDependencies": { "@bufbuild/buf-darwin-arm64": "1.71.0", "@bufbuild/buf-darwin-x64": "1.71.0", "@bufbuild/buf-linux-aarch64": "1.71.0", "@bufbuild/buf-linux-armv7": "1.71.0", "@bufbuild/buf-linux-x64": "1.71.0", "@bufbuild/buf-win32-arm64": "1.71.0", "@bufbuild/buf-win32-x64": "1.71.0" }, "bin": { "buf": "bin/buf", "protoc-gen-buf-lint": "bin/protoc-gen-buf-lint", "protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking" } }, "sha512-GDcjBCwLgHT/4nX4YSnYatZ7sDZDpHV6dxQvoT2/P6gKvV23O6hl8NryzLIRKmeau0FRXpQKHVy1dMfnBSpy+w=="],
"@bufbuild/buf-darwin-arm64": ["@bufbuild/buf-darwin-arm64@1.71.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qZ7xZQyen/jOKFPVs3dlN9pMA56PI4YEo3r4/9ixtiH9gyFgfowR31axsocUgXGThjiN8mvOA8WfpG2tvaSvsw=="],
@@ -1144,9 +1170,9 @@
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.6", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA=="],
"@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
"@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
@@ -1244,15 +1270,15 @@
"@firebase/webchannel-wrapper": ["@firebase/webchannel-wrapper@1.0.3", "", {}, "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ=="],
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
"@floating-ui/react": ["@floating-ui/react@0.27.19", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog=="],
"@floating-ui/react": ["@floating-ui/react@0.27.20", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.9", "@floating-ui/utils": "^0.2.12", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
"@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.9", "", {}, "sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ=="],
@@ -1624,6 +1650,8 @@
"@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.3", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg=="],
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
"@mermaid-js/parser": ["@mermaid-js/parser@1.2.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA=="],
"@microsoft/fast-element": ["@microsoft/fast-element@1.14.0", "", {}, "sha512-zXvuSOzvsu8zDTy9eby8ix8VqLop2rwKRgp++ZN2kTCsoB3+QJVoaGD2T/Cyso2ViZQFXNpiNCVKfnmxBvmWkQ=="],
@@ -1666,21 +1694,21 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@openai/codex": ["@openai/codex@0.130.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.130.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.130.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.130.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.130.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.130.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.130.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-WGDj+RZ3TXWC/7MlwprgLWOqzpwatPIINPhP3IRzHA0ni+o3QZ4i4xrS2uWwGmHUJ395J5JHwoZAAZYyfJyz6w=="],
"@openai/codex": ["@openai/codex@0.144.1", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.144.1-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.144.1-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.144.1-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.144.1-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.144.1-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.144.1-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-Xir1zqPfpenhdoAoshN53uonzbBXj18COyzRkFlVZpSNyEl5XtkuYu9oddELePFN7K/0sXUcSO34Ad5IeCXPbw=="],
"@openai/codex-darwin-arm64": ["@openai/codex@0.130.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R9pkGC7kwC8yQ8el5hvBlmugQlcsG/pHMEFgZluu03X9fD2TezGxdq3KqRDRCZuMYl07ILamVEoqknuJ0cq7MA=="],
"@openai/codex-darwin-arm64": ["@openai/codex@0.144.1-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dABeDK+ATqMG54MGBd3VjpKfh5EOoqx9PKVQB2QYDaEXx3F6CdUCXue5QIMfr4OxziUj8pUcLAQyd+KFqiTUFw=="],
"@openai/codex-darwin-x64": ["@openai/codex@0.130.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-gJ+7J8djevgtdra+NgDAiQQPW+O3KTsgGfE3E5dpDfww3zS5OCeV0V2dhxqnJdlOjOSDw99o0P2LqBv19mhpRw=="],
"@openai/codex-darwin-x64": ["@openai/codex@0.144.1-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-K2g3Q3tNxzFhV0SuzO6HcsYK7EQrp/o4HyeReyhkwVrwwUPoYwyIbB0IRjHIiDzRhbKriDccid2iyF5aPqdTcg=="],
"@openai/codex-linux-arm64": ["@openai/codex@0.130.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-tFtH0V9/hEI3d9y7zP92BXI9FM4Z3+STNQaOR52Czv18TRtCFUp7CbIUYaToopuq6UBfnE1VKr8RLhwT5FcbmA=="],
"@openai/codex-linux-arm64": ["@openai/codex@0.144.1-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-451o15+XtaXCCb35t/KCyyPqXHnTPxPxtdqEYOnE3e4sH5AfnI/uVJwfdjOksMG6vRLy6R+fLvSDOMguRFLmQw=="],
"@openai/codex-linux-x64": ["@openai/codex@0.130.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-3VcNlez99xdnEf+kB1IOpWv9fICYV9PiGj4sLCO4TCcShLnyxe+YBGa3poknkvXLnMG0qiN9SMnYS2FGrMxQcA=="],
"@openai/codex-linux-x64": ["@openai/codex@0.144.1-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-HNGVI+BulrOaC/0IzBvd6EL62j7LrlbFKibrhw6hZjjCjAeUYzRB2jB4qDzXN1NfqDi6Xrvniof3kwbwab24lg=="],
"@openai/codex-win32-arm64": ["@openai/codex@0.130.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-vdpmiNp57L/arZabltLXn8TyEtNa7W1meOEkr+3R6W/8ZyBt++wuqz1Orv134OT2grrcFJsIVCAIPiqUxCvBkA=="],
"@openai/codex-win32-arm64": ["@openai/codex@0.144.1-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-L4aDVEh9o1u7WYoxpSyv3un9Bz26YZYocOFqE2oHdEQDL2s6/LdtutLQc3oUZruLlEbkNsjSU0HI1OKsP0+Ctg=="],
"@openai/codex-win32-x64": ["@openai/codex@0.130.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-FzMznm7fr5/nbjZgOujZ9Y9AbdGm7ji1FOoWiY3U+srqauvZaTgn6o6aCheSL7kuymu7nTLOO/cAyWV6NuesqQ=="],
"@openai/codex-win32-x64": ["@openai/codex@0.144.1-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-qv2HOp6v/nVP31p5I5GxYyL0wa79PMzim1+W9CKSV0UldjFV9AMbualA8PeXcYhbvvh9Y1UASXxwjuQdlyfAvw=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.17.15", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-gWRQOEggHTELJ9+BtelxnuAczk9qutCXVZenPgRPaT8oVxePf52jfWbqfhyFjnrN8Vlp8tCCTdkEpFW5pZAuEA=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.17.18", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-c/C9PhY8PrbcxDY+JIYtOZsrmMD0KzoVvxq+RGUrZ6LQp57SuVBbT4lfwA2G8Se5RNC1N5JtYjiuaXeECnF2SQ=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
@@ -1740,7 +1768,7 @@
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.9.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.9.0", "@opentelemetry/core": "2.9.0", "@opentelemetry/sdk-trace-base": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ=="],
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.42.0", "", {}, "sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw=="],
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
"@opentui-ui/dialog": ["@opentui-ui/dialog@0.1.2", "", { "peerDependencies": { "@opentui/core": "^0.1.69", "@opentui/react": "^0.1.69", "@opentui/solid": "^0.1.69" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-EZ4FG5u5sxU75+6pcsJsLzsD5JqO05So/1ceZUKUu7nxZ9IF7gcZEi+MU4HnYC9cb2Q7w6hM9y3/iW+jE1C53w=="],
@@ -1772,7 +1800,7 @@
"@playwright/test": ["@playwright/test@1.61.1", "", { "dependencies": { "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" } }, "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig=="],
"@posthog/core": ["@posthog/core@1.40.0", "", { "dependencies": { "@posthog/types": "^1.393.0" } }, "sha512-oGDbIwlTquNwdHbEL5ZLEkuW4UFkkEanfx3QAxDgyVbISv+OAA6YGQwrvo0JD3MUJEbJZvyh8XsX+WYDGw9XHw=="],
"@posthog/core": ["@posthog/core@1.40.2", "", { "dependencies": { "@posthog/types": "^1.393.0" } }, "sha512-H12j7O9iHGvpK9t2ko8W4pvfbV1pBDxrsWC1LA6yp2RhzwvC4T3sWhu+AekDQJSRSrJEWlB0t/Ueq9QhPSq7FQ=="],
"@posthog/types": ["@posthog/types@1.393.0", "", {}, "sha512-vzWeEJZ7ERQhFRoQYaP5jzN1JvIu46UJyHXsuv+dTGW2r3sMgREOhNxXLZjmFHwZ8/FOHQoyqqQmXTCXZSfMSg=="],
@@ -2260,7 +2288,7 @@
"@simple-git/argv-parser": ["@simple-git/argv-parser@1.1.1", "", { "dependencies": { "@simple-git/args-pathspec": "^1.0.3" } }, "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.52", "", {}, "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw=="],
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
@@ -2278,21 +2306,21 @@
"@slack/web-api": ["@slack/web-api@7.19.0", "", { "dependencies": { "@slack/logger": "^4.0.1", "@slack/types": "^2.21.0", "@types/node": ">=18", "@types/retry": "0.12.0", "axios": "^1.16.0", "eventemitter3": "^5.0.1", "form-data": "^4.0.4", "is-electron": "2.2.2", "is-stream": "^2", "p-queue": "^6", "p-retry": "^4", "retry": "^0.13.1" } }, "sha512-ItjyjEZml+LDH8CjcCLRLJHh7VZtevPKExrRN3l5KWyBliyDnGAeoO4Y+K+fFBmRpKLYVPgqWMX4THldv2HVtA=="],
"@smithy/core": ["@smithy/core@3.29.2", "", { "dependencies": { "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-DXUk6yU0C1Q1tYvJh1VCtl8QOBcSoZpKwjTPkxT6A4MUQYHvgeKGByL8mrEdxnvhdf9nq5GyzmRb5n/vPgu3Lw=="],
"@smithy/core": ["@smithy/core@3.29.3", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.7", "", { "dependencies": { "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-UEMLOoA0Fl4uYBxh6l0uN0H6EJe/A89OGeDNTteQeXpJ20BcpfIr4wlCY9pel1jEAUHAxaYwuqrYlrKdXE1GKQ=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.8", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.4.7", "", { "dependencies": { "@smithy/core": "^3.29.2", "tslib": "^2.6.2" } }, "sha512-YNodWVjMFOMAyjQgpHBBCz62DbYu4xwhpt+z5HRf7OZPwrfHgNDCyUZxaC0fy9/2TWcO+niOMHSl8aMgfbWZTQ=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.4.8", "", { "dependencies": { "@smithy/core": "^3.29.3", "tslib": "^2.6.2" } }, "sha512-Q0qYaae6vcDW9JRyMbdXiONnbL+uNNExKzy7sippUzI1CRCfiCELGOR5tR0FQmMciJfsLgox5K82kl6BmwrnAg=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-psnst7NZWdAEvJvyW8YZEE7xNVMyLrQFfHtyrVFrxNyy+dKWkQ+rqC6oI5ZhxThpUy9RSfEshgm34zqbOxzsRw=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-SuqeisTyPoiIPtIYru/sGxGyXzmZ+8nnFOhC+qRPglt06Ebd1yH//CDltZB2J/3WBNVhwfUaZ0EtHB3cm2X32g=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.4", "", { "dependencies": { "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-BNTop/fSOptmoVk8g+efwHCofFh37g70OWGAFES1TeAAJja1K5aAI8rTE26ETSc5k8IQuWY2kAIoPla01NgYrA=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.6.3", "", { "dependencies": { "@smithy/core": "^3.29.2", "@smithy/types": "^4.16.0", "tslib": "^2.6.2" } }, "sha512-8qVKKzqh7naF27ePmx0SkUfnGP/wBI9dyaeAmhHvopnbIlItUAmB/e6PkPCU3rRb2v9BY8D4EZXSoydSibatvw=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="],
"@smithy/types": ["@smithy/types@4.16.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-aVUabzlBBmY0PfvVgLKQSOGFIL5/7R54JE3uD9a5Ay/jSED61SkuAcCYENNXJzYUvJ1NPrWO0P+rAXHCkbBUKw=="],
"@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="],
"@smithy/util-utf8": ["@smithy/util-utf8@4.4.7", "", { "dependencies": { "@smithy/core": "^3.29.2", "tslib": "^2.6.2" } }, "sha512-NskAyOBZcHO+fa1HwkwWuPbMUQE7NZ4IWBnAc71E4f9IdoQ65WLkdJ2ed/n4Q8vePSgZUN/CbyGZLBiI7dMI+w=="],
"@smithy/util-utf8": ["@smithy/util-utf8@4.4.8", "", { "dependencies": { "@smithy/core": "^3.29.3", "tslib": "^2.6.2" } }, "sha512-hwVELJTTRUqwrEvMI73PwsP27cO1HgkAKDoKelhMS3biTd8z5iXj76UF7oemuDba3X5eHZqjedeyBUhJLns+Kg=="],
"@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="],
@@ -2302,12 +2330,18 @@
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@storybook/addon-a11y": ["@storybook/addon-a11y@9.1.20", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^9.1.20" } }, "sha512-VFZ34y4ApmFwIzPRs2OJrG6jtYhM5y91eCZLTlR/HMGQciKF4TdOJHjj+5vf91SOER5UDcLizXetpiUowiZSgw=="],
"@storybook/addon-docs": ["@storybook/addon-docs@9.1.20", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "9.1.20", "@storybook/icons": "^1.4.0", "@storybook/react-dom-shim": "9.1.20", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^9.1.20" } }, "sha512-eUIOd4u/p9994Nkv8Avn6r/xmS7D+RNmhmu6KGROefN3myLe3JfhSdimal2wDFe/h/OUNZ/LVVKMZrya9oEfKQ=="],
"@storybook/builder-vite": ["@storybook/builder-vite@9.1.20", "", { "dependencies": { "@storybook/csf-plugin": "9.1.20", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^9.1.20", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-cdU3Q2/wEaT8h+mApFToRiF/0hYKH1eAkD0scQn67aODgp7xnkr0YHcdA+8w0Uxd2V7U8crV/cmT/HD0ELVOGw=="],
"@storybook/csf-plugin": ["@storybook/csf-plugin@9.1.20", "", { "dependencies": { "unplugin": "^1.3.1" }, "peerDependencies": { "storybook": "^9.1.20" } }, "sha512-HHgk50YQhML7mT01Mzf9N7lNMFHWN4HwwRP90kPT9Ct+Jhx7h3LBDbdmWjI96HwujcpY7eoYdTfpB1Sw8Z7nBQ=="],
"@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="],
"@storybook/icons": ["@storybook/icons@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" } }, "sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw=="],
"@storybook/react": ["@storybook/react@9.1.20", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "9.1.20" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.1.20", "typescript": ">= 4.9.x" }, "optionalPeers": ["typescript"] }, "sha512-TJhqzggs7HCvLhTXKfx8HodnVq9YizsB2J31s9v6olU0UCxbCY+FYaCF+XdE8qUCyefGRZgHKzGBIczJ/q9e2g=="],
"@storybook/react-dom-shim": ["@storybook/react-dom-shim@9.1.20", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.1.20" } }, "sha512-UYdZavfPwHEqCKMqPssUOlyFVZiJExLxnSHwkICSZBmw3gxXJcp1aXWs7PvoZdWz2K4ztl3IcKErXXHeiY6w+A=="],
@@ -2556,7 +2590,7 @@
"@types/get-folder-size": ["@types/get-folder-size@3.0.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-tSf/k7Undx6jKRwpChR9tl+0ZPf0BVwkjBRtJ5qSnz6iWm2ZRYMAS2MktC2u7YaTAFHmxpL/LBxI85M7ioJCSg=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
@@ -2578,6 +2612,8 @@
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mdx": ["@types/mdx@2.0.14", "", {}, "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg=="],
"@types/mocha": ["@types/mocha@10.0.10", "", {}, "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
@@ -2662,7 +2698,7 @@
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.6", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.2", "", {}, "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
@@ -2754,13 +2790,13 @@
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ai": ["ai@6.0.221", "", { "dependencies": { "@ai-sdk/gateway": "3.0.145", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.37", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cB7qJbNTMuD5spdJEo+guejX0rkjhSQpc4PHITNB+iBFBnGYHLUZOM+uSeIkY4mS4sVVKBKm3kSQQoI5cUwU3g=="],
"ai": ["ai@6.0.224", "", { "dependencies": { "@ai-sdk/gateway": "3.0.148", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-plo+hHwMANM+P6FjX8RN6W8c8so5NVZGwvDGCWGWwIbvI5tc0ZV1zhr9v/Kq78zyxKZWE50YCSYbqGiPFEhGww=="],
"ai-sdk-ollama": ["ai-sdk-ollama@3.8.8", "", { "dependencies": { "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.30", "jsonrepair": "^3.14.0", "ollama": "^0.6.3" }, "peerDependencies": { "ai": "^6.0.197" } }, "sha512-peWelPf6sVsRULQyYhfyu1dMZhwewRszsbQVfbuhNLflh+ncRXn6pe1BRE3NAcSsWd7JMRZAV5RzcuR3R9ZfaQ=="],
"ai-sdk-provider-claude-code": ["ai-sdk-provider-claude-code@3.5.1", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "@anthropic-ai/claude-agent-sdk": "^0.3.170" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-DxbOp3qIQTAhdvtynhW3Eq+NqAuU8UKVRVzBSdmVnCVMke8xC372Hy/j7FDalC5PkOOdiv7jI9yUzIk6vN1l6g=="],
"ai-sdk-provider-claude-code": ["ai-sdk-provider-claude-code@3.5.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "@anthropic-ai/claude-agent-sdk": "^0.3.205" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-+uzxoU4X3ldzi14jbNvQ9SIitEmjXUM8wYwBC9xb7nfF0W03OB+EvzGrFMwkiE9mVSkFijXIDFzKBhMQ5zkqcg=="],
"ai-sdk-provider-codex-cli": ["ai-sdk-provider-codex-cli@1.2.2", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "jsonc-parser": "^3.3.1" }, "optionalDependencies": { "@openai/codex": "^0.130.0" }, "peerDependencies": { "zod": "^3.0.0 || ^4.0.0" } }, "sha512-hlIWo9KP7/hJaEjXZbxgjVO/FvMn3I+5RSht0PBp3GOBKVFkKeKlrIDvaA8Wi/nAYiAePESPem+fi1NVNfNQJA=="],
"ai-sdk-provider-codex-cli": ["ai-sdk-provider-codex-cli@1.3.1", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "jsonc-parser": "^3.3.1" }, "optionalDependencies": { "@openai/codex": "^0.144.0" }, "peerDependencies": { "zod": "^3.0.0 || ^4.0.0" } }, "sha512-edTG3jkjF32ChGGctyivRi1Vyd/vLx6ktUuvlBTA5ennu4J3eKesLphKu+Lzv80nJQhrsqFXjM1aC1NfZc7elw=="],
"ai-sdk-provider-opencode-sdk": ["ai-sdk-provider-opencode-sdk@3.0.6", "", { "dependencies": { "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.15", "@opencode-ai/sdk": "^1.2.15" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CZ2I5z96HUdEKAT1ExhdxLE1bjD8efzT7I5GjBevWgwNGgYZjMPE7ZfQCJFVAzWqODwdTdxp7KSZjnmcd4h1Bw=="],
@@ -2826,6 +2862,8 @@
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
"axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="],
"axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="],
"azure-devops-node-api": ["azure-devops-node-api@12.5.0", "", { "dependencies": { "tunnel": "0.0.6", "typed-rest-client": "^1.8.4" } }, "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og=="],
@@ -2848,7 +2886,7 @@
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.42", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="],
"basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="],
@@ -2888,7 +2926,7 @@
"browser-stdout": ["browser-stdout@1.3.1", "", {}, "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="],
"browserslist": ["browserslist@4.28.5", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001800", "electron-to-chromium": "^1.5.387", "node-releases": "^2.0.50", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ=="],
"browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="],
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
@@ -2932,7 +2970,7 @@
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001803", "", {}, "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001805", "", {}, "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA=="],
"case-anything": ["case-anything@2.1.13", "", {}, "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng=="],
@@ -3234,7 +3272,7 @@
"discord-interactions": ["discord-interactions@4.4.0", "", {}, "sha512-jjJx8iwAeJcj8oEauV43fue9lNqkf38fy60aSs2+G8D1nJmDxUIrk08o3h0F3wgwuBWWJUZO+X/VgfXsxpCiJA=="],
"discord.js": ["discord.js@14.26.4", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.1", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.40", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA=="],
"discord.js": ["discord.js@14.26.5", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.1", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.48", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-SGYgjiAs0o8ZzMC97XmFXKONyairJ9YzVda+LvoSKs9YYh2gPtQhY+liaV6H/w72jxYbu9ggiSqAXoF3FLOH4A=="],
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
@@ -3248,7 +3286,7 @@
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"dompurify": ["dompurify@3.4.11", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw=="],
"dompurify": ["dompurify@3.4.12", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
@@ -3332,7 +3370,7 @@
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
"eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
@@ -3402,7 +3440,7 @@
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
"fast-equals": ["fast-equals@5.4.1", "", {}, "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ=="],
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
@@ -3622,7 +3660,7 @@
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
"hono": ["hono@4.12.28", "", {}, "sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA=="],
"hono": ["hono@4.12.29", "", {}, "sha512-1hNiRjawYrLq/4m3DQQjPGFg0VZkk4RjQJDff/excI6Dm9BiL75qxGrd7/c6YOxPdq6AscP3LiXhQ6fKFC1Waw=="],
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
@@ -3658,7 +3696,7 @@
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
"image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="],
@@ -4196,7 +4234,7 @@
"node-pty": ["node-pty@1.2.0-beta.11", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-THcUyu1WwdgoIyUvgXOZ70EOMXzheGa0q3tbEb5kUIfKgcpBJ+AJ9Q1kq0bKtYmQzr77usXiTORZTLmAUQlnoQ=="],
"node-releases": ["node-releases@2.0.50", "", {}, "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg=="],
"node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="],
"node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="],
@@ -4244,7 +4282,7 @@
"open-graph-scraper": ["open-graph-scraper@6.12.0", "", { "dependencies": { "chardet": "^2.2.0", "cheerio": "^1.2.0", "iconv-lite": "^0.7.2", "undici": "^7.28.0" } }, "sha512-x0fS3eHxdCox+rFBhQSVe+qBznSPn1pspp8A4BoaVEkiECZEwagEb8z06swLfaFFE2gefj1BvEBeJmdeGTDnYw=="],
"openai": ["openai@6.45.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw=="],
"openai": ["openai@6.46.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-DFg6jEPT2RO+oAyXtddeUJU8zkGy1OQ1AjGzNIJUMQG03TTqvCpy9tBpQ+2VVVnvrl3E56F8GEin2JYtWpITtA=="],
"opentui-spinner": ["opentui-spinner@0.0.6", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="],
@@ -4370,15 +4408,15 @@
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
"postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
"postcss": ["postcss@8.5.17", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"posthog-js": ["posthog-js@1.399.0", "", { "dependencies": { "@posthog/core": "^1.40.0", "@posthog/types": "^1.393.0", "core-js": "^3.49.0", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.29.3", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.3.0" } }, "sha512-8l+uZJZM3+OAc0D0+iLBMDRVfWF9s26Rt0jv8EC3kMJcA/9oyOets0zDgqIZk2TTfUs3H96ycLE6Lwj1wWG16Q=="],
"posthog-js": ["posthog-js@1.399.2", "", { "dependencies": { "@posthog/core": "^1.40.1", "@posthog/types": "^1.393.0", "core-js": "^3.49.0", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.29.3", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.3.0" } }, "sha512-xcvrGEgUYtIVcWPRlVfc/NkMo0IP9nwnM/dzJIAzjDOSewkdDk/9T4Vz1+gooEhXdQCPfG44jSK95mVJsoySqA=="],
"posthog-node": ["posthog-node@5.40.0", "", { "dependencies": { "@posthog/core": "^1.39.6" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-DrLfHuauO0W6qruF80iqr5JdmLysef74XzOB4eh36oRLRhxCySLraTqsi2Pj161LZnp9/JNdRDxwT8ei8VK2YA=="],
"posthog-node": ["posthog-node@5.41.0", "", { "dependencies": { "@posthog/core": "^1.40.2" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-jOkX6THOr5WD+FGUEaTxekas8c7NOC3TqJ2Byfe2KMimQdL/F/osz17uSbkNzR4V9WFZoe8YaGP3Xp0EUpPKGg=="],
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
@@ -4654,7 +4692,7 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.9.0", "", {}, "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA=="],
"shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="],
"shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="],
@@ -4808,7 +4846,7 @@
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"systeminformation": ["systeminformation@5.31.15", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-7mqCtD28TK5dVdLAQONVa/Do/NBgMH2dxqf49nh6DIKoEWuDg6tkgGBP+dN22VEJVPZa/QqiHomhWNRn4WUNTQ=="],
"systeminformation": ["systeminformation@5.31.16", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-37NsFaeaqwYxanLgdJAGWj8OXIRgvSBsDXGhlC0uUoHyl9nf4HrNsZjqtQ9Gc99bj9FQpffzYccPVuQ6gpjfqg=="],
"tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
@@ -5046,7 +5084,7 @@
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
"vite": ["vite@8.1.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.16", "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA=="],
"vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
@@ -5194,6 +5232,8 @@
"@cline/code/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@cline/ui/vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
"@cline/vscode-rollout/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
"@cline/vscode-rollout/@types/vscode": ["@types/vscode@1.84.0", "", {}, "sha512-lCGOSrhT3cL+foUEqc8G1PVZxoDbiMmxgnUZZTEnHF4mC47eKAUtBGAuMLY6o6Ua8PAuNCoKXbqPmJd1JYnQfg=="],
+2
View File
@@ -50,6 +50,7 @@ ClinePass includes the following models, tested and benchmarked for coding agent
| Model | Model ID |
|-------|------------|
| GLM-5.2 | `cline-pass/glm-5.2` |
| Kimi K3 | `cline-pass/kimi-k3` |
| Kimi K2.7 Code | `cline-pass/kimi-k2.7-code` |
| Kimi K2.6 | `cline-pass/kimi-k2.6` |
| DeepSeek V4 Pro | `cline-pass/deepseek-v4-pro` |
@@ -91,6 +92,7 @@ ClinePass is a flat monthly subscription, so you are not charged the individual
| Model | Input | Output | Cached Read | Cached Write |
|-------|-------|--------|-------------|--------------|
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - |
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
| DeepSeek V4 Pro | $1.74 | $3.48 | $0.0145 | - |
+20
View File
@@ -1,5 +1,25 @@
# Cline SDK Changelog
## 0.0.65
- Claude Code and Codex provider SDKs are now optional peer dependencies loaded on demand, dramatically cutting install size
- Added Kimi K3 to the bundled ClinePass model fallback
- Runs now retry once after refreshing expired OAuth credentials
- Team runs: the spawn tool is no longer exposed to teammate agents
- Team runs: errored teammate runs now report as failed instead of completed
- Improved shell-command parsing to fix a Windows shell mismatch
- New `@cline/ui` agent chat components with Storybook and npm packaging
- Updated the bundled model catalog
## 0.0.64
- Improved max output token handling across providers (gateway routing, OpenAI vendor, and reasoning models)
- Frontmatter and user-instruction files that start with a UTF-8 byte order mark (e.g. saved by Windows editors) now parse correctly
## 0.0.63
- The session runtime now emits `task.mistake_limit_reached` telemetry when the consecutive-mistake limit is hit, so every host (CLI, VS Code extension, hub daemon) captures it — including auto-stops when no host prompt is configured
## 0.0.62
- Fixed Ollama native API routing so context window and timeout settings work again
+1
View File
@@ -68,6 +68,7 @@
"!@cline/core/telemetry",
"!@cline/core/rpc",
"!@cline/shared/browser",
"!@cline/shared/node",
"!@cline/shared/types",
"!@cline/shared/storage",
"!@cline/shared/db"
@@ -13,6 +13,7 @@ import {
type AgentToolContext,
ClineCore,
createTool,
stripUtf8Bom,
} from "@cline/core";
import YAML from "yaml";
import { z } from "zod";
@@ -179,6 +180,9 @@ function parseFrontmatter(md: string): {
data: Record<string, unknown>;
body: string;
} {
// stripUtf8Bom keeps the frontmatter match below working for files saved with a leading
// UTF-8 BOM (see cline/cline#12151).
md = stripUtf8Bom(md);
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!m) return { data: {}, body: md.trim() };
try {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cline/agents",
"version": "0.0.62",
"version": "0.0.65",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline",
@@ -69,6 +69,50 @@ describe("AgentRuntime", () => {
expect(model.requests).toHaveLength(1);
});
it("fails a turn that hits the model output token limit before completion", async () => {
const logger = {
debug: vi.fn(),
log: vi.fn(),
error: vi.fn(),
};
const model = new ScriptedModel([
() => [
{ type: "reasoning-delta", text: "thinking..." },
{ type: "finish", reason: "max-tokens" },
],
]);
const runtime = new AgentRuntime({ model, logger });
const result = await runtime.run("Hi");
expect(result.status).toBe("failed");
expect(result.error?.message).toContain("maximum output token limit");
expect(model.requests).toHaveLength(1);
expect(result.messages).toHaveLength(2);
expect(result.messages.at(-1)).toMatchObject({
role: "assistant",
content: [{ type: "reasoning", text: "thinking..." }],
});
expect(logger.log).toHaveBeenCalledWith(
"Agent loop caught error",
expect.objectContaining({
severity: "error",
status: "failed",
errorMessage: expect.stringContaining("maximum output token limit"),
iteration: 1,
assistantContentPartCount: 1,
}),
);
expect(logger.error).toHaveBeenCalledWith(
"Agent run failed",
expect.objectContaining({
error: expect.objectContaining({
message: expect.stringContaining("maximum output token limit"),
}),
}),
);
});
it("does not persist an empty assistant message when the model stream fails", async () => {
const model = new ScriptedModel([
() => [{ type: "finish", reason: "error", error: "upstream failed" }],
@@ -1725,6 +1769,14 @@ describe("AgentRuntime", () => {
expect(result.status).toBe("failed");
expect(events).toContain("run-failed");
expect(logger.log).toHaveBeenCalledWith(
"Agent loop caught error",
expect.objectContaining({
severity: "error",
status: "failed",
errorMessage: "model failed",
}),
);
expect(logger.error).toHaveBeenCalled();
expect(telemetry.capture).toHaveBeenCalled();
});
+21 -5
View File
@@ -34,6 +34,9 @@ import {
} from "@cline/shared";
import { nanoid } from "nanoid";
const MAX_TOKENS_INCOMPLETE_TURN_MESSAGE =
"Model reached the maximum output token limit before completing the turn";
// Local `createUID` helper. The clinee source imports this from
// `@cline/shared` (see `packages/shared/dist/identifier.ts`), but
// sdk-re's shared package does not expose it yet. Inlining here keeps
@@ -645,6 +648,9 @@ export class AgentRuntime {
finishReason,
});
if (finishReason === "max-tokens" && toolCalls.length === 0) {
throw new Error(MAX_TOKENS_INCOMPLETE_TURN_MESSAGE);
}
if (finishReason === "error" && toolCalls.length === 0) {
throw new Error(this.state.lastError ?? "Model stream failed");
}
@@ -718,23 +724,33 @@ export class AgentRuntime {
const normalized =
error instanceof Error ? error : new Error(String(error));
const isControlledStop = normalized instanceof ControlledStopError;
const status =
this.abortController.signal.aborted || isControlledStop
? "aborted"
: "failed";
const isAborted = this.abortController.signal.aborted || isControlledStop;
const status = isAborted ? "aborted" : "failed";
this.state.status = status;
this.state.lastError = normalized.message;
const lastAssistantMessage = this.findLastAssistantMessage();
const result: AgentRunResult = {
agentId: this.state.agentId,
agentRole: this.state.agentRole,
runId: this.state.runId ?? createUID("run"),
status,
iterations: this.state.iteration,
outputText: textFromMessage(this.findLastAssistantMessage()),
outputText: textFromMessage(lastAssistantMessage),
messages: cloneMessages(this.state.messages),
usage: cloneUsage(this.state.usage),
error: status === "failed" ? normalized : undefined,
};
this.config.logger?.log?.("Agent loop caught error", {
severity: status === "failed" ? "error" : "warn",
agentId: this.state.agentId,
agentRole: this.state.agentRole,
runId: result.runId,
status,
iteration: this.state.iteration,
errorName: normalized.name,
errorMessage: normalized.message,
assistantContentPartCount: lastAssistantMessage?.content.length ?? 0,
});
await this.callAfterRunHooks(result);
if (status === "failed") {
await this.emit({
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cline/core",
"description": "Cline Core SDK for Node Runtime",
"version": "0.0.62",
"version": "0.0.65",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline",
@@ -86,6 +86,24 @@ ${content}`);
expect(updateSkillMarkdownEnabledState(content, true)).toBe(content);
});
// Regression test for https://github.com/cline/cline/issues/12151: a leading UTF-8 BOM
// (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not prevent frontmatter
// from being recognized when toggling a skill's enabled state.
it("disables a skill whose content starts with a UTF-8 BOM", () => {
const content = `\uFEFF---
name: code-review
description: Review code carefully
---
First line.`;
const updated = updateSkillMarkdownEnabledState(content, false);
const parsed = parseSkillConfigFromMarkdown(updated, "fallback");
expect(parsed.disabled).toBe(true);
expect(parsed.frontmatter.name).toBe("code-review");
expect(parsed.frontmatter.description).toBe("Review code carefully");
});
it("writes toggled content and returns the resulting state", async () => {
const tempRoot = await mkdtemp(join(tmpdir(), "core-skill-toggle-"));
tempRoots.push(tempRoot);
@@ -1,4 +1,5 @@
import { readFile, writeFile } from "node:fs/promises";
import { stripUtf8Bom } from "@cline/shared";
import YAML from "yaml";
export interface ToggleSkillFrontmatterOptions {
@@ -19,10 +20,15 @@ interface MarkdownFrontmatterParts {
}
function parseMarkdownFrontmatter(content: string): MarkdownFrontmatterParts {
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
// regex below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
const normalizedContent = stripUtf8Bom(content);
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
const match = content.match(frontmatterRegex);
const match = normalizedContent.match(frontmatterRegex);
if (!match) {
return { data: {}, body: content, hadFrontmatter: false };
return { data: {}, body: normalizedContent, hadFrontmatter: false };
}
const [, yamlContent, body] = match;
@@ -137,6 +137,23 @@ Document rollout and rollback steps.`,
expect(workflow.disabled).toBe(true);
});
// Regression test for https://github.com/cline/cline/issues/12151: a leading UTF-8 BOM
// (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not prevent frontmatter
// from being recognized.
it("parses markdown frontmatter when the content starts with a UTF-8 BOM", () => {
const skill = parseSkillConfigFromMarkdown(
`\uFEFF---
name: my-skill
description: A test skill
---
This is a test skill.`,
"fallback",
);
expect(skill.name).toBe("my-skill");
expect(skill.description).toBe("A test skill");
expect(skill.instructions).toBe("This is a test skill.");
});
it("emits typed events for skills, rules, and workflows in one watcher", async () => {
const tempRoot = await mkdtemp(
join(tmpdir(), "core-user-instructions-loader-"),
@@ -1,5 +1,6 @@
import { readdir, readFile, stat } from "node:fs/promises";
import { basename, dirname, extname, join, resolve } from "node:path";
import { stripUtf8Bom } from "@cline/shared";
import {
AGENTS_RULES_FILE_NAME,
RULES_CONFIG_DIRECTORY_NAME,
@@ -193,10 +194,15 @@ async function discoverManagedPluginRoots(
function parseMarkdownFrontmatter(
content: string,
): ParseMarkdownFrontmatterResult {
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
// regex below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
const normalizedContent = stripUtf8Bom(content);
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
const match = content.match(frontmatterRegex);
const match = normalizedContent.match(frontmatterRegex);
if (!match) {
return { data: {}, body: content, hadFrontmatter: false };
return { data: {}, body: normalizedContent, hadFrontmatter: false };
}
const [, yamlContent, body] = match;
@@ -211,7 +217,7 @@ function parseMarkdownFrontmatter(
const message = error instanceof Error ? error.message : String(error);
return {
data: {},
body: content,
body: normalizedContent,
hadFrontmatter: true,
parseError: message,
};
@@ -5,6 +5,7 @@ import {
getToolContextTelemetry,
} from "../../services/telemetry/tool-context";
import {
buildRunCommandsDescription,
createDefaultTools,
createReadFilesTool,
createSearchTool,
@@ -480,6 +481,71 @@ describe("default apply_patch tool", () => {
});
});
describe("run_commands tool description", () => {
it("names PowerShell with ';' sequencing for PowerShell shells", () => {
const description = buildRunCommandsDescription("powershell", true);
expect(description).toContain("Commands run through PowerShell");
expect(description).toContain("use ';' to sequence commands");
expect(description).toContain("in Windows environment");
});
it("names cmd.exe with '&&' sequencing for cmd shells", () => {
const description = buildRunCommandsDescription("cmd", true);
expect(description).toContain("Commands run through cmd.exe");
expect(description).toContain("use '&&' to sequence commands");
expect(description).not.toContain("PowerShell");
});
it("describes WSL bash with the /mnt working-directory mapping", () => {
const description = buildRunCommandsDescription("wsl", true);
expect(description).toContain("bash in WSL");
expect(description).toContain("/mnt/<drive>");
expect(description).not.toContain("PowerShell");
});
it("notes the Windows host for POSIX shells on Windows only", () => {
const onWindows = buildRunCommandsDescription("posix", true);
expect(onWindows).toContain("POSIX (bash-compatible) shell on Windows");
expect(onWindows).not.toContain("PowerShell");
const onUnix = buildRunCommandsDescription("posix", false);
expect(onUnix).not.toContain("Windows");
expect(onUnix).toContain("grep/head/tail");
});
it("derives the createShellTool description from config.shell", () => {
const posixTool = createShellTool(async () => "ok", {
shell: "/bin/bash",
});
expect(posixTool.description).toContain(
"Run non-interactive shell commands",
);
expect(posixTool.description).not.toContain("PowerShell");
const cmdTool = createShellTool(async () => "ok", {
shell: "C:\\Windows\\System32\\cmd.exe",
});
expect(cmdTool.description).toContain("Commands run through cmd.exe");
});
it("re-derives the description on each read when config.shell is a provider", () => {
let shell = "/bin/bash";
const tool = createShellTool(async () => "ok", {
shell: () => shell,
});
expect(tool.description).not.toContain("PowerShell");
shell = "powershell.exe";
expect(tool.description).toContain("Commands run through PowerShell");
// The property must survive the shallow copy the runtime performs when
// building AgentToolDefinitions for a model request.
shell = "cmd.exe";
const definition = { ...tool };
expect(definition.description).toContain("Commands run through cmd.exe");
});
});
describe("default run_commands tool", () => {
function createTelemetryStub(): ITelemetryService {
return {
@@ -8,6 +8,9 @@ import {
type AgentTool,
type AgentToolContext,
createTool,
getDefaultShell,
getShellKind,
type ShellKind,
validateWithZod,
zodToJsonSchema,
} from "@cline/shared";
@@ -395,15 +398,64 @@ const RUN_COMMANDS_SHARED_INSTRUCTIONS =
"Use for listing files, checking git status, running builds, executing tests, etc. " +
"Commands must be non-interactive. Commands that require follow-up input like pagers should be skipped or used with supported flags/env (e.g. git --no-pager, --non-interactive) to bypass the interaction steps. ";
/**
* Build the run_commands tool description for the shell that will actually
* execute the commands. The shell kind decides the syntax guidance (quoting,
* sequencing, heredocs), and isWindows adds environment context for POSIX
* shells running on a Windows host (e.g. Git Bash).
*/
export function buildRunCommandsDescription(
shellKind: ShellKind,
isWindows: boolean,
): string {
if (shellKind === "powershell" || shellKind === "cmd") {
const shellName = shellKind === "powershell" ? "PowerShell" : "cmd.exe";
const sequencingOperator = shellKind === "powershell" ? "';'" : "'&&'";
return (
"Run non-interactive shell commands from the root of the workspace in Windows environment. " +
RUN_COMMANDS_SHARED_INSTRUCTIONS +
`Output beyond ~${Math.round(MAX_COMMAND_OUTPUT_CHARS / 1000)}k characters is middle-truncated (start and end preserved); filter output when you need specific sections. ` +
`Commands run through ${shellName}; quote paths and arguments for ${shellName} and use ${sequencingOperator} to sequence commands. ` +
"Include multiple commands in the same call when they are independent and safe to run concurrently. When independent reads, searches, or edits are also needed, call those tools in the same response."
);
}
const environmentNote =
shellKind === "wsl"
? "Commands run through bash in WSL (wsl.exe); the Windows working directory is mounted under /mnt/<drive>. "
: isWindows
? "Commands run through a POSIX (bash-compatible) shell on Windows. "
: "";
return (
"Run non-interactive shell commands from the root of the workspace. " +
RUN_COMMANDS_SHARED_INSTRUCTIONS +
environmentNote +
"Commands should be properly shell-escaped and targeted to avoid error or timeout. Include multiple commands in the same call when they are independent complete shell commands and safe to run concurrently; multiline scripts and heredocs must be a single command string. When independent reads, searches, or edits are also needed, call those tools in the same response. " +
`Output beyond ~${Math.round(MAX_COMMAND_OUTPUT_CHARS / 1000)}k characters is middle-truncated (start and end preserved); pipe through grep/head/tail when you need specific sections of large output. ` +
"For long-running commands, run them in background and redirect output to a tmp file that you can read from later."
);
}
/**
* Create the run_commands shell tool for the current platform.
*
* This preserves the SDK's platform-specific prompting/schema choices while
* exposing a single generic shell-tool factory for host integrations.
* exposing a single generic shell-tool factory for host integrations. Pass
* config.shell (matching the executor's shell) so the syntax guidance in the
* tool description matches the shell that actually runs the commands.
*
* config.shell may be a provider function instead of a string. The runtime
* reads `description` when building each model request, so a provider is
* consulted at that boundary: a shell change made while the model is
* generating does not affect the request in flight, and the next request
* names the new shell. The provider must return the shell the executor will
* use for tool calls issued by that next request.
*/
export function createShellTool(
executor: ShellExecutor,
config: Pick<DefaultToolsConfig, "cwd" | "bashTimeoutMs"> = {},
config: Pick<DefaultToolsConfig, "cwd" | "bashTimeoutMs"> & {
shell?: string | (() => string);
} = {},
): AgentTool<unknown, ToolOperationResult[]> {
const timeoutMs = config.bashTimeoutMs ?? 30000;
const timeoutSource =
@@ -412,19 +464,17 @@ export function createShellTool(
: "configured_setting";
const cwd = config.cwd ?? process.cwd();
const isWindows = process.platform === "win32";
const configShell = config.shell;
const resolveShell =
typeof configShell === "function"
? configShell
: () => configShell ?? getDefaultShell(process.platform);
const describe = () =>
buildRunCommandsDescription(getShellKind(resolveShell()), isWindows);
return createTool<unknown, ToolOperationResult[]>({
const tool = createTool<unknown, ToolOperationResult[]>({
name: "run_commands",
description: isWindows
? "Run non-interactive shell commands from the root of the workspace in Windows environment. " +
RUN_COMMANDS_SHARED_INSTRUCTIONS +
`Output beyond ~${Math.round(MAX_COMMAND_OUTPUT_CHARS / 1000)}k characters is middle-truncated (start and end preserved); filter output when you need specific sections. ` +
"Commands run through PowerShell; quote paths and arguments for PowerShell and use ';' to sequence commands. Include multiple commands in the same call when they are independent and safe to run concurrently. When independent reads, searches, or edits are also needed, call those tools in the same response."
: "Run non-interactive shell commands from the root of the workspace. " +
RUN_COMMANDS_SHARED_INSTRUCTIONS +
"Commands should be properly shell-escaped and targeted to avoid error or timeout. Include multiple commands in the same call when they are independent complete shell commands and safe to run concurrently; multiline scripts and heredocs must be a single command string. When independent reads, searches, or edits are also needed, call those tools in the same response. " +
`Output beyond ~${Math.round(MAX_COMMAND_OUTPUT_CHARS / 1000)}k characters is middle-truncated (start and end preserved); pipe through grep/head/tail when you need specific sections of large output. ` +
"For long-running commands, run them in background and redirect output to a tmp file that you can read from later.",
description: describe(),
inputSchema: zodToJsonSchema(RunCommandsInputSchema),
timeoutMs: timeoutMs * 2,
retryable: false,
@@ -443,6 +493,17 @@ export function createShellTool(
});
},
});
if (typeof configShell === "function") {
// The runtime rebuilds tool definitions from this property for every
// model request, so a getter re-derives the description at exactly the
// send-to-model boundary. AgentTool consumers only read `description`.
Object.defineProperty(tool, "description", {
get: describe,
enumerable: true,
});
}
return tool;
}
/**
@@ -0,0 +1,86 @@
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { EventEmitter } from "node:events";
import type { AgentToolContext } from "@cline/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
const spawn = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", () => ({ spawn }));
import { createBuiltinTools } from "./index";
const context: AgentToolContext = {
agentId: "agent-1",
conversationId: "conversation-1",
iteration: 1,
};
function createSuccessfulChildProcess(): ChildProcessWithoutNullStreams {
const child = Object.assign(new EventEmitter(), {
stdout: new EventEmitter(),
stderr: new EventEmitter(),
stdin: new EventEmitter(),
pid: 123,
kill: vi.fn(() => true),
});
queueMicrotask(() => child.emit("close", 0));
return child as unknown as ChildProcessWithoutNullStreams;
}
async function executeRunCommands(
options: Parameters<typeof createBuiltinTools>[0],
) {
const tool = createBuiltinTools(options).find(
(candidate) => candidate.name === "run_commands",
);
if (!tool) {
throw new Error("Expected run_commands tool");
}
await tool.execute({ commands: ["echo ok"] }, context);
return tool;
}
describe("createBuiltinTools shell configuration", () => {
beforeEach(() => {
spawn.mockReset();
spawn.mockImplementation(() => createSuccessfulChildProcess());
});
it.each([
{
name: "top-level shell",
options: { shell: "cmd.exe" },
expectedShell: "cmd.exe",
expectedDescription: "Commands run through cmd.exe",
},
{
name: "executor shell",
options: { executorOptions: { bash: { shell: "powershell.exe" } } },
expectedShell: "powershell.exe",
expectedDescription: "Commands run through PowerShell",
},
{
name: "top-level shell precedence",
options: {
shell: "cmd.exe",
executorOptions: { bash: { shell: "powershell.exe" } },
},
expectedShell: "cmd.exe",
expectedDescription: "Commands run through cmd.exe",
},
])("uses the $name for both description and execution", async ({
options,
expectedShell,
expectedDescription,
}) => {
const tool = await executeRunCommands(options);
expect(tool.description).toContain(expectedDescription);
expect(spawn).toHaveBeenCalledWith(
expectedShell,
expect.any(Array),
expect.any(Object),
);
});
});
@@ -122,7 +122,7 @@ export type {
// Convenience: Create Tools with Built-in Executors
// =============================================================================
import type { AgentTool } from "@cline/shared";
import { type AgentTool, getDefaultShell } from "@cline/shared";
import { createDefaultTools } from "./definitions";
import {
createDefaultExecutors,
@@ -136,11 +136,16 @@ import type { CreateDefaultToolsOptions, ToolExecutors } from "./types";
export interface CreateBuiltinToolsOptions
extends Omit<CreateDefaultToolsOptions, "executors"> {
/**
* Configuration for the built-in executors
* Configuration for the built-in executors. `bash.shell` is used when the
* top-level `shell` option is not set; the top-level option takes precedence.
*/
executorOptions?: DefaultExecutorsOptions;
/**
* Optional executor overrides/additions for tools without built-ins
* Optional executor overrides/additions for tools without built-ins.
* An overriding `bash` executor replaces the built-in one wholesale: it
* decides its own shell, and the resolved `shell` option only shapes the
* run_commands description. Overriders must honor that shell themselves
* to keep the description truthful.
*/
executors?: Partial<ToolExecutors>;
}
@@ -180,14 +185,29 @@ export function createBuiltinTools(
executors: executorOverrides,
...toolsConfig
} = options;
// The top-level shell is the public tool configuration and takes precedence
// over the legacy executor-specific location. Resolve it once so prompting
// and execution cannot disagree.
const shell =
toolsConfig.shell ??
executorOptions.bash?.shell ??
getDefaultShell(process.platform);
const resolvedExecutorOptions: DefaultExecutorsOptions = {
...executorOptions,
bash: {
...executorOptions.bash,
shell,
},
};
const executors = {
...createDefaultExecutors(executorOptions),
...createDefaultExecutors(resolvedExecutorOptions),
...(executorOverrides ?? {}),
};
return createDefaultTools({
...toolsConfig,
shell,
executors,
});
}
@@ -23,6 +23,23 @@ You are a code reviewer.`);
});
});
// Regression test for https://github.com/cline/cline/issues/12151: a leading UTF-8 BOM
// (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not prevent frontmatter
// from being recognized.
it("parses frontmatter when the content starts with a UTF-8 BOM", () => {
const config = parseConfiguredAgentConfig(`\uFEFF---
name: code-reviewer
description: Reviews code
---
You are a code reviewer.`);
expect(config).toMatchObject({
name: "code-reviewer",
description: "Reviews code",
systemPrompt: "You are a code reviewer.",
});
});
it("does not treat delimiter lines in the body as frontmatter delimiters", () => {
const config = parseConfiguredAgentConfig(`---
name: code-reviewer
@@ -1,5 +1,6 @@
import { type Dirent, existsSync, readdirSync, readFileSync } from "node:fs";
import { basename, extname, join } from "node:path";
import { stripUtf8Bom } from "@cline/shared";
import { resolveAgentConfigSearchPaths } from "@cline/shared/storage";
import YAML from "yaml";
import { z } from "zod";
@@ -40,6 +41,11 @@ function splitFrontmatter(content: string): {
frontmatter: string;
body: string;
} {
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
// match below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
content = stripUtf8Bom(content);
const firstLineMatch = content.match(/^(---)[^\S\r\n]*(?:\r?\n|$)/);
if (!firstLineMatch) {
throw new Error("Missing YAML frontmatter block in agent config file.");
@@ -24,6 +24,7 @@ export type DelegatedAgentConnectionConfig = Pick<
| "apiKey"
| "baseUrl"
| "headers"
| "onAuthError"
| "providerConfig"
| "knownModels"
| "thinking"
@@ -88,6 +89,7 @@ export function createDelegatedAgentConfigProvider(
apiKey: runtimeConfig.apiKey,
baseUrl: runtimeConfig.baseUrl,
headers: runtimeConfig.headers,
onAuthError: runtimeConfig.onAuthError,
providerConfig: runtimeConfig.providerConfig,
knownModels: runtimeConfig.knownModels,
thinking: runtimeConfig.thinking,
@@ -647,3 +647,61 @@ describe("AgentTeamsRuntime teammate lifecycle events", () => {
}
});
});
describe("AgentTeamsRuntime run failure reporting", () => {
it("marks a run failed when the teammate result finishes with an error", async () => {
const events: TeamEvent[] = [];
// biome-ignore lint/complexity/useArrowFunction: `new SessionRuntime(...)` requires a non-arrow callable.
createSessionRuntimeMock.mockImplementationOnce(function () {
return {
abort: vi.fn(),
run: vi.fn(async () => ({
text: "Unauthorized: Please re-authenticate your Cline account.",
iterations: 8,
finishReason: "error",
durationMs: 100,
usage: {
inputTokens: 10,
outputTokens: 20,
cacheReadTokens: 0,
cacheWriteTokens: 0,
totalCost: 0,
},
messages: [],
})),
continue: vi.fn(),
canStartRun: vi.fn(() => true),
getAgentId: vi.fn(() => "teammate-1"),
getConversationId: vi.fn(() => "conv-1"),
getMessages: vi.fn(() => []),
subscribeEvents: vi.fn(() => () => {}),
};
});
const runtime = new AgentTeamsRuntime({
teamName: "test-team",
onTeamEvent: (event) => events.push(event),
});
runtime.spawnTeammate({
agentId: "alice",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-5-20250929",
systemPrompt: "Helper teammate",
tools: [],
},
});
const run = runtime.startTeammateRun("alice", "do the thing");
const settledRun = await runtime.awaitRun(run.id, 1);
expect(settledRun.status).toBe("failed");
expect(settledRun.error).toContain("Unauthorized");
expect(events).toContainEqual(
expect.objectContaining({
type: TeamMessageType.RunFailed,
run: expect.objectContaining({ id: run.id, status: "failed" }),
}),
);
});
});
@@ -1218,6 +1218,13 @@ export class AgentTeamsRuntime {
taskId: run.taskId,
continueConversation: run.continueConversation,
});
// Model-stream failures surface as results with finishReason
// "error" rather than throws; route them through the failure
// path so the run is reported as failed (and retried when
// maxRetries allows) instead of masquerading as completed.
if (result.finishReason === "error") {
throw new Error(result.text || "Teammate run failed");
}
run.status = "completed";
run.result = result;
run.endedAt = new Date();
@@ -230,6 +230,10 @@ function spawnTeamTeammate(
teammateConfigProvider: options.teammateConfigProvider,
createBaseTools: options.createBaseTools,
allowSpawn: false,
// Spawning is lead-only; exposing the tool to teammates just
// makes them burn turns on "Only the lead agent can manage
// teammates." rejections.
includeSpawnTool: false,
}),
);
options.runtime.spawnTeammate({
@@ -297,6 +297,14 @@ export interface DefaultToolsConfig {
*/
cwd?: string;
/**
* Shell executable (name or full path) the run_commands executor will use.
* The tool description tells the model which shell syntax to write, so this
* must match the shell configured on the executor.
* @default getDefaultShell(process.platform) "/bin/bash" on Unix, "powershell" on Windows
*/
shell?: string;
/**
* Timeout for file read operations in milliseconds
* @default 10000
+7 -5
View File
@@ -112,6 +112,7 @@ export {
parseUserCommandEnvelope,
registerDisposable,
SDK_ERROR_TELEMETRY_EVENT,
stripUtf8Bom,
} from "@cline/shared";
export * from "@cline/shared/storage";
export {
@@ -136,11 +137,6 @@ export {
type UserRemoteConfigOrganization,
type UserRemoteConfigResponse,
} from "./account";
export {
hashSecret,
setSdkLogger,
sdkDebug,
} from "./logging/early-logger";
export {
createOAuthClientCallbacks,
type OAuthClientCallbacksOptions,
@@ -406,6 +402,11 @@ export type {
export * from "./hub";
export { HubRuntimeHost } from "./hub/runtime-host/hub-runtime-host";
export { RemoteRuntimeHost } from "./hub/runtime-host/remote-runtime-host";
export {
hashSecret,
sdkDebug,
setSdkLogger,
} from "./logging/early-logger";
export {
buildRemoteConfigSessionBlobUploadMetadata,
createRemoteConfigSessionMessagesArtifactUploader,
@@ -651,6 +652,7 @@ export {
captureMentionFailed,
captureMentionSearchResults,
captureMentionUsed,
captureMistakeLimitReached,
captureModeSwitch,
captureProviderApiError,
captureProviderConfigured,
@@ -495,6 +495,27 @@ export class LocalRuntimeHost implements RuntimeHost {
configWithProvider.teamName = runtime.teamRuntime.getTeamName();
}
// Auth-retry hook for every agent in the session (lead, teammates,
// subagents): refresh OAuth credentials and propagate the new key to
// all connections, then let the runtime retry the failed run. Without
// this, a token that expires while the lead is blocked (e.g. in
// team_await_runs) kills teammate runs with a raw provider 401.
const onAuthError = async (): Promise<boolean> => {
const liveSession = this.sessions.get(sessionId);
if (!liveSession || !isOAuthProvider(liveSession.config.providerId)) {
return false;
}
try {
await this.syncOAuthCredentials(liveSession, { forceRefresh: true });
return true;
} catch {
return false;
}
};
runtime.delegatedAgentConfigProvider?.updateConnectionDefaults({
onAuthError,
});
const tools = [...runtime.tools, ...(configWithProvider.extraTools ?? [])];
const extensions = runtime.extensions ?? bootstrap.extensions;
const explicitInitialCompactionState = startInput.initialCompactionState;
@@ -564,6 +585,7 @@ export class LocalRuntimeHost implements RuntimeHost {
apiKey: providerConfig.apiKey,
baseUrl: providerConfig.baseUrl,
headers: providerConfig.headers,
onAuthError,
knownModels: providerConfig.knownModels,
providerConfig,
thinking: configWithProvider.thinking,
@@ -2189,6 +2189,61 @@ describe("SessionRuntime.run — tracker wiring (P1 #3)", () => {
expect(abortCalls).toHaveLength(0);
});
it("captures task.mistake_limit_reached telemetry exactly once when the limit is hit", async () => {
const capture = vi.fn();
const telemetry = {
capture,
captureRequired: vi.fn(),
setDistinctId: vi.fn(),
setMetadata: vi.fn(),
updateMetadata: vi.fn(),
setCommonProperties: vi.fn(),
updateCommonProperties: vi.fn(),
isEnabled: vi.fn(() => true),
recordCounter: vi.fn(),
recordHistogram: vi.fn(),
recordGauge: vi.fn(),
flush: vi.fn(async () => {}),
dispose: vi.fn(async () => {}),
};
const { deps } = makeScriptedRuntime({
events: failedToolTurnEvents(),
});
const session = new SessionRuntime(
makeAgentConfig({
execution: { maxConsecutiveMistakes: 2 },
sessionId: "sess_mistakes",
telemetry,
}),
deps,
);
await session.run("one");
// First failed turn — counter 1 < 2, no telemetry yet.
const limitEvents = () =>
capture.mock.calls
.map((call) => call[0])
.filter((event) => event.event === "task.mistake_limit_reached");
expect(limitEvents()).toHaveLength(0);
await session.continue("two");
// Second failed turn hits the limit: exactly one event, even though
// no `onConsecutiveMistakeLimitReached` callback is configured (the
// tracker falls back to the default stop decision).
const events = limitEvents();
expect(events).toHaveLength(1);
expect(events[0].properties).toMatchObject({
ulid: "sess_mistakes",
model: "claude-3-5-sonnet",
provider: "anthropic",
reason: "tool_execution_failed",
consecutiveMistakes: 2,
maxConsecutiveMistakes: 2,
isSubagent: false,
});
expect(events[0].properties.agentId).toMatch(/^agent_/);
});
it("aborts on hard-threshold loop detection of identical tool calls", async () => {
const identical = (i: number): AgentRuntimeEvent => ({
type: "tool-started",
@@ -2342,3 +2397,90 @@ describe("SessionRuntime.run — tracker wiring (P1 #3)", () => {
expect(abortCalls).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// Auth retry
// ---------------------------------------------------------------------------
describe("SessionRuntime auth retry", () => {
const authFailure: Partial<AgentRunResult> = {
status: "failed",
error: new Error(
"Unauthorized: Please make sure you're using the latest version of Cline and re-authenticate your Cline account.",
),
};
/** Runtime factory that scripts each successive AgentRuntime build. */
function withSequencedRuntimes(scripts: FakeAgentRuntimeScript[]): {
deps: SessionRuntimeOrchestratorDeps;
createdCount: () => number;
} {
let created = 0;
const deps: SessionRuntimeOrchestratorDeps = {
createAgentRuntimeImpl: () => {
const script = scripts[Math.min(created, scripts.length - 1)];
created += 1;
return makeFakeAgentRuntime(script).runtime;
},
};
return { deps, createdCount: () => created };
}
it("retries once with refreshed credentials when a run fails with an auth error", async () => {
const onAuthError = vi.fn(async () => true);
const capture = vi.fn();
const telemetry = { capture } as unknown as AgentConfig["telemetry"];
const { deps, createdCount } = withSequencedRuntimes([
{ result: authFailure },
{ result: { outputText: "recovered" } },
]);
const session = new SessionRuntime(
makeAgentConfig({ onAuthError, telemetry }),
deps,
);
const result = await session.run("go");
expect(onAuthError).toHaveBeenCalledTimes(1);
expect(createdCount()).toBe(2);
expect(result.finishReason).toBe("completed");
expect(result.text).toBe("recovered");
expect(capture).toHaveBeenCalledWith({
event: "user.auth_run_retry",
properties: { provider: "anthropic", recovered: true },
});
});
it("returns the failed result when the host cannot refresh credentials", async () => {
const onAuthError = vi.fn(async () => false);
const { deps, createdCount } = withSequencedRuntimes([
{ result: authFailure },
]);
const session = new SessionRuntime(makeAgentConfig({ onAuthError }), deps);
const result = await session.run("go");
expect(onAuthError).toHaveBeenCalledTimes(1);
expect(createdCount()).toBe(1);
expect(result.finishReason).toBe("error");
});
it("does not invoke onAuthError for non-auth failures", async () => {
const onAuthError = vi.fn(async () => true);
const { deps, createdCount } = withSequencedRuntimes([
{
result: {
status: "failed",
error: new Error("Model stream failed"),
},
},
]);
const session = new SessionRuntime(makeAgentConfig({ onAuthError }), deps);
const result = await session.run("go");
expect(onAuthError).not.toHaveBeenCalled();
expect(createdCount()).toBe(1);
expect(result.finishReason).toBe("error");
});
});
@@ -39,6 +39,7 @@ import {
type ContributionRegistry,
createContributionRegistry,
type ITelemetryService,
isLikelyAuthError,
type LegacyAgentUsage,
type LoopDetectionConfig,
type Message,
@@ -52,6 +53,10 @@ import {
createAgentModelFromConfig,
resolveKnownModelsFromConfig,
} from "../../services/llms/handler-factory";
import {
captureAuthRunRetry,
captureMistakeLimitReached,
} from "../../services/telemetry/core-events";
import { CLINE_INTERNAL_TELEMETRY_METADATA_KEY } from "../../services/telemetry/tool-context";
import {
getMessageBuilderOptionsFromEnv,
@@ -276,10 +281,10 @@ export class SessionRuntime {
private readonly agentId: string;
private readonly parentAgentId?: string;
private readonly logger?: BasicLogger;
// Reserved for §3.4.4 telemetry parity (not yet consumed — §3.4.4
// listed as explicitly deferred until telemetry wiring is added).
// Typed as `readonly` to preserve the field slot for future use
// without re-touching the constructor.
// §3.4.4 telemetry parity. Currently consumed by the MistakeTracker's
// `onLimitTelemetry` hook (task.mistake_limit_reached); most other
// runtime telemetry is emitted host-side from the agent event stream
// (services/agent-events.ts).
readonly telemetry?: ITelemetryService;
private readonly conversation: ConversationStore;
private readonly mistakeTracker: MistakeTracker;
@@ -401,6 +406,22 @@ export class SessionRuntime {
this.mistakeTracker = new MistakeTracker({
maxConsecutiveMistakes: maxMistakes,
onLimitReached: config.onConsecutiveMistakeLimitReached,
onLimitTelemetry: (context) => {
// Read connection fields from `this.config` at fire time so a
// mid-session `updateConnection` is reflected in the event.
captureMistakeLimitReached(this.telemetry, {
ulid: this.config.sessionId ?? this.conversation.getConversationId(),
model: this.config.modelId,
provider: this.config.providerId,
reason: context.reason,
consecutiveMistakes: context.consecutiveMistakes,
maxConsecutiveMistakes: context.maxConsecutiveMistakes,
agentId: this.agentId,
conversationId: this.conversation.getConversationId(),
parentAgentId: this.parentAgentId,
isSubagent: Boolean(this.parentAgentId),
});
},
emit: (event) => this.emitLegacyEvent(event),
log: (level, message, metadata) =>
leveledLog(this.logger, level, message, metadata),
@@ -674,7 +695,7 @@ export class SessionRuntime {
isContinue: boolean;
}): Promise<AgentResult> {
let activePromise!: Promise<AgentResult>;
activePromise = this.executeRunInternal(input).finally(() => {
activePromise = this.executeRunWithAuthRetry(input).finally(() => {
if (this.activeRunPromise === activePromise) {
this.activeRunPromise = null;
}
@@ -683,6 +704,37 @@ export class SessionRuntime {
return activePromise;
}
/**
* Retry a run once when it failed with an auth-like error and the host
* refreshed credentials via `config.onAuthError`. The failed attempt's
* trail is already persisted to the conversation store, so the retry
* continues from where the stream died instead of replaying the run.
*/
private async executeRunWithAuthRetry(input: {
userMessage?: string;
userImages?: string[];
userFiles?: string[];
isContinue: boolean;
}): Promise<AgentResult> {
const result = await this.executeRunInternal(input);
if (
result.finishReason !== "error" ||
!this.config.onAuthError ||
!isLikelyAuthError(result.text)
) {
return result;
}
const refreshed = await this.config.onAuthError().catch(() => false);
if (!refreshed) {
return result;
}
const retryResult = await this.executeRunInternal({ isContinue: true });
captureAuthRunRetry(this.telemetry, this.config.providerId, {
recovered: retryResult.finishReason !== "error",
});
return retryResult;
}
private async executeRunInternal(input: {
userMessage?: string;
userImages?: string[];
@@ -60,6 +60,12 @@ export interface MistakeTrackerOptions {
) =>
| Promise<ConsecutiveMistakeLimitDecision>
| ConsecutiveMistakeLimitDecision;
/**
* Observability hook fired exactly once per limit hit, right before the
* limit decision is resolved regardless of whether `onLimitReached` is
* configured or what it decides. Used for telemetry.
*/
readonly onLimitTelemetry?: (ctx: ConsecutiveMistakeLimitContext) => void;
readonly emit: (event: AgentEvent) => void;
readonly log: LeveledLog;
readonly agentId: string;
@@ -107,14 +113,16 @@ export class MistakeTracker {
return { action: "continue" };
}
const limitContext: ConsecutiveMistakeLimitContext = {
iteration: input.iteration,
consecutiveMistakes: next,
maxConsecutiveMistakes: max,
reason: input.reason,
details: input.details,
};
this.options.onLimitTelemetry?.(limitContext);
const decision = await resolveConsecutiveMistakeDecision(
{
iteration: input.iteration,
consecutiveMistakes: next,
maxConsecutiveMistakes: max,
reason: input.reason,
details: input.details,
},
limitContext,
this.options.onLimitReached,
);
@@ -9,6 +9,7 @@ import {
captureCompactionExecuted,
captureCompactionSkipped,
captureExtensionActivated,
captureMistakeLimitReached,
captureProviderConfigured,
captureRunCommandsTimeout,
captureTelemetryOptOut,
@@ -270,6 +271,35 @@ describe("captureWorkspacePathResolved", () => {
).not.toThrow();
});
});
describe("captureMistakeLimitReached", () => {
const baseProps = {
ulid: "sess-1",
model: "claude-3-5-sonnet",
provider: "anthropic",
reason: "tool_execution_failed",
consecutiveMistakes: 3,
maxConsecutiveMistakes: 3,
};
test("emits task.mistake_limit_reached with limit context and a timestamp", () => {
const stub = createTelemetryStub();
captureMistakeLimitReached(stub.telemetry, baseProps);
expect(stub.capture).toHaveBeenCalledTimes(1);
expect(stub.captureRequired).not.toHaveBeenCalled();
const { event, properties } = captureCallAt(stub, 0);
expect(event).toBe("task.mistake_limit_reached");
expect(properties).toMatchObject(baseProps);
expect(typeof properties?.timestamp).toBe("string");
});
test("no-ops when telemetry is undefined", () => {
expect(() =>
captureMistakeLimitReached(undefined, baseProps),
).not.toThrow();
});
});
describe("captureCompactionExecuted", () => {
const baseProps = {
ulid: "ulid-1",
@@ -49,6 +49,7 @@ export const CORE_TELEMETRY_EVENTS = {
AUTH_FAILED: "user.auth_failed",
AUTH_LOGGED_OUT: "user.auth_logged_out",
AUTH_REFRESH_SOFT_FAILURE: "user.auth_refresh_soft_failure",
AUTH_RUN_RETRY: "user.auth_run_retry",
PROVIDER_CONFIGURED: "user.provider_configured",
TELEMETRY_OPT_OUT: "user.opt_out",
},
@@ -63,6 +64,7 @@ export const CORE_TELEMETRY_EVENTS = {
SKILL_USED: "task.skill_used",
DIFF_EDIT_FAILED: "task.diff_edit_failed",
PROVIDER_API_ERROR: "task.provider_api_error",
MISTAKE_LIMIT_REACHED: "task.mistake_limit_reached",
MENTION_USED: "task.mention_used",
MENTION_FAILED: "task.mention_failed",
MENTION_SEARCH_RESULTS: "task.mention_search_results",
@@ -290,6 +292,23 @@ export function captureAuthRefreshSoftFailure(
});
}
/**
* Fires when a run failed with an auth-like error, credentials were
* refreshed, and the run was retried once. `recovered: true` means the retry
* completed a run that would previously have surfaced a raw provider 401
* (e.g. a teammate stranded on a spawn-time token snapshot past its TTL).
*/
export function captureAuthRunRetry(
telemetry: ITelemetryService | undefined,
provider?: string,
details?: { recovered?: boolean },
): void {
emit(telemetry, CORE_TELEMETRY_EVENTS.USER.AUTH_RUN_RETRY, {
provider,
recovered: details?.recovered,
});
}
/**
* Fires when the user finishes configuring a "bring your own provider"
* (API-key based) provider during onboarding or via settings.
@@ -490,6 +509,28 @@ export function captureProviderApiError(
});
}
/**
* Records when the consecutive mistake limit is reached, right before the
* limit decision (host prompt / auto-stop) is resolved.
*/
export function captureMistakeLimitReached(
telemetry: ITelemetryService | undefined,
properties: {
ulid: string;
model: string;
provider?: string;
/** What kind of mistake tripped the limit. */
reason: string;
consecutiveMistakes: number;
maxConsecutiveMistakes: number;
} & Partial<TelemetryAgentIdentityProperties>,
): void {
emit(telemetry, CORE_TELEMETRY_EVENTS.TASK.MISTAKE_LIMIT_REACHED, {
...properties,
timestamp: new Date().toISOString(),
});
}
export function captureRunCommandsTimeout(
telemetry: ITelemetryService | undefined,
properties: RunCommandsTimeoutTelemetryProperties,
+14 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@cline/llms",
"version": "0.0.62",
"version": "0.0.65",
"description": "Config-driven SDK for selecting, extending, and instantiating LLM providers and models",
"repository": {
"type": "git",
@@ -41,6 +41,12 @@
"peerDependenciesMeta": {
"@aws-sdk/client-bedrock-runtime": {
"optional": true
},
"ai-sdk-provider-claude-code": {
"optional": true
},
"ai-sdk-provider-codex-cli": {
"optional": true
}
},
"dependencies": {
@@ -61,14 +67,18 @@
"@streamparser/json": "^0.0.21",
"ai": "^6.0.144",
"ai-sdk-ollama": "^3.8.8",
"ai-sdk-provider-claude-code": "^3.4.3",
"ai-sdk-provider-codex-cli": "^1.1.0",
"ai-sdk-provider-opencode-sdk": "^3.0.1",
"dify-ai-provider": "^1.1.0",
"nanoid": "^5.1.7",
"zod": "^4.3.6"
},
"peerDependencies": {
"@aws-sdk/client-bedrock-runtime": "^3.0.0"
"@aws-sdk/client-bedrock-runtime": "^3.0.0",
"ai-sdk-provider-claude-code": "^3.4.3",
"ai-sdk-provider-codex-cli": "^1.1.0"
},
"devDependencies": {
"ai-sdk-provider-claude-code": "^3.4.3",
"ai-sdk-provider-codex-cli": "^1.1.0"
}
}
+8 -5
View File
@@ -77,13 +77,16 @@ while generating the catalog. Conceptually:
safeOutputTokens = min(
modelReportedMaxOutput,
contextWindow - estimatedPromptTokens - reserveTokens,
userConfiguredOutputCap,
userConfiguredOutputCap or productDefaultOutputCap,
)
```
The SDK gateway only sends an output token limit when the caller provides
`request.options.maxTokens` or an equivalent host configuration. Catalog
metadata does not become a request parameter by itself.
The SDK gateway resolves an output token limit for the provider request. It uses
`request.options.maxTokens` or an equivalent host configuration when present,
otherwise it applies the product default output cap
(`DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS`, currently 32000) when the model catalog has
either an output limit or a context window. Provider modules are responsible for
forwarding that limit only to wire API surfaces that support it.
The exact request-limit policy belongs in the provider/gateway/core request
path, not in generated catalog data.
@@ -161,5 +164,5 @@ and observable.
- `catalog-live.test.ts`: tests catalog normalization behavior.
- `catalog.generated.ts`: checked-in generated provider/model catalog.
- `../../scripts/generate-models.ts`: writes generated catalog output.
- `../providers/ai-sdk.ts`: passes `maxOutputTokens` into AI SDK.
- `../providers/ai-sdk.ts`: conditionally passes `maxOutputTokens` into AI SDK.
- `../providers/gateway.ts`: resolves per-request/default `maxTokens`.
File diff suppressed because it is too large Load Diff
+25 -9
View File
@@ -15,7 +15,7 @@ import {
parseJsonStream,
sanitizeSurrogates,
} from "@cline/shared";
import { jsonSchema, NoSuchToolError, streamText } from "ai";
import { type CallSettings, jsonSchema, NoSuchToolError, streamText } from "ai";
import { nanoid } from "nanoid";
import { extractErrorMessage } from "./format";
import {
@@ -53,6 +53,18 @@ interface GatewayNormalizedUsage {
}
type ProviderModuleKind = AiSdkProviderOptionsTarget;
export function buildAiSdkStreamConfig(
request: GatewayStreamRequest,
_context: GatewayProviderContext,
): Partial<CallSettings> {
return {
...(request.maxTokens !== undefined
? { maxOutputTokens: request.maxTokens }
: {}),
temperature: request.temperature,
};
}
function buildCachedAiSdkMessages(
request: GatewayStreamRequest,
context: GatewayProviderContext,
@@ -352,10 +364,15 @@ function toAiSdkMessages(
}
}
// A message left empty only because its reasoning was dropped is
// omitted entirely instead of forwarded as an empty turn.
const emptiedByDroppedReasoning = !includeReasoning && skippedReasoning;
if (content.length > 0) {
normalizedMessages.push({ role: message.role, content });
} else if (!includeReasoning && skippedReasoning) {
} else if (message.role === "user" || message.role === "assistant") {
} else if (
!emptiedByDroppedReasoning &&
(message.role === "user" || message.role === "assistant")
) {
normalizedMessages.push({ role: message.role, content: "" });
}
}
@@ -1178,6 +1195,9 @@ function createAiSdkProvider(kind: ProviderModuleKind): GatewayProviderFactory {
context,
kind,
) as never;
const requestConfig = provider.buildStreamConfig
? provider.buildStreamConfig(request, context)
: buildAiSdkStreamConfig(request, context);
recordProviderRequestCapture({
stage: "ai_sdk_prompt",
request,
@@ -1186,8 +1206,7 @@ function createAiSdkProvider(kind: ProviderModuleKind): GatewayProviderFactory {
...(useSystemOption ? { system: systemPrompt } : {}),
tools,
providerOptions,
maxOutputTokens: request.maxTokens,
temperature: request.temperature,
...requestConfig,
},
});
stream = streamText({
@@ -1195,16 +1214,13 @@ function createAiSdkProvider(kind: ProviderModuleKind): GatewayProviderFactory {
messages: messages as never,
...(useSystemOption ? { system: systemPrompt } : {}),
tools: tools as never,
temperature: request.temperature,
...(request.maxTokens !== undefined
? { maxOutputTokens: request.maxTokens }
: {}),
abortSignal: request.signal,
experimental_repairToolCall: repairMalformedToolCall as never,
experimental_telemetry: {
isEnabled: langfuse,
},
providerOptions,
...requestConfig,
onError: ({ error: streamError }) => {
const msg = extractErrorMessage(streamError);
capturedError.current = msg;
+8 -1
View File
@@ -9,6 +9,7 @@ import {
type ProviderCapability,
type ProviderConfigField,
} from "@cline/shared";
import { GENERATED_PROVIDER_MODELS } from "../catalog/catalog.generated";
import { getGeneratedModelsForProvider } from "../catalog/catalog.generated-access";
import {
isCanonicalModelIdForAliasRules,
@@ -307,7 +308,13 @@ function generatedModels(providerId: string): Record<string, ModelInfo> {
}
function firstGeneratedModelId(providerId: string): string {
const generatedModelList = Object.keys(generatedModels(providerId));
// Use the catalog's authored order, not release-date order. The cline-pass
// block mirrors the recommended-models endpoint, which lists the intended
// default subscription model first — the newest model is not necessarily a
// safe default.
const generatedModelList = Object.keys(
GENERATED_PROVIDER_MODELS.providers[providerId] ?? {},
);
if (!generatedModelList.length) {
return "";
}
@@ -5,6 +5,7 @@ import {
toGatewayRequestMessages,
} from "./compat";
import { ClineNotSubscribedError } from "./errors";
import { DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS } from "./gateway";
import type { Message } from "./types";
const streamTextSpy = vi.fn();
@@ -361,7 +362,7 @@ describe("createGatewayApiHandler.createMessage", () => {
openaiCompatibleSpy.mockClear();
});
it("does not convert catalog maxTokens into request maxOutputTokens", async () => {
it("uses the default maxOutputTokens without expanding to catalog maxTokens", async () => {
streamTextSpy.mockReturnValue({
fullStream: (async function* () {
yield { type: "finish", finishReason: "stop" };
@@ -395,7 +396,10 @@ describe("createGatewayApiHandler.createMessage", () => {
const call = streamTextSpy.mock.calls.at(-1)?.[0] as
| { maxOutputTokens?: unknown }
| undefined;
expect(call).not.toHaveProperty("maxOutputTokens");
expect(call).toHaveProperty(
"maxOutputTokens",
DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS,
);
});
it("sends configured OpenAI-compatible maxOutputTokens to the provider request", async () => {

Some files were not shown because too many files have changed in this diff Show More