* fix(sdk): set versioned Cline client-identity headers for Cline provider
* address feedback
* feat: add platform metadata to client context
Include platform, platformVersion, and isMultiRoot in extension client
context for CLI, ACP, and VS Code sessions. This provides downstream
core/session logic with richer runtime information and distinguishes ACP
clients from the standard CLI client.
* lint
* clean up
* fix: resolve client host identity via HostProvider for standalone compatibility
cline-session-factory.ts is also bundled into the standalone cline-core
(JetBrains), where the 'vscode' module resolves to the generated Proxy-stub
module: vscode.env.appName and vscode.version return Proxy objects, which
would flow into X-PLATFORM/X-PLATFORM-VERSION header values and fail at
request serialization.
Resolve the identity through HostProvider.env.getHostVersion() instead —
the VS Code hostbridge returns the identical values (vscode.env.appName,
vscode.version, ClineClient.VSCode, extension version), and JetBrains'
hostbridge returns its real host values, so the standalone stops reporting
itself as the VS Code extension as a bonus. Multi-root detection goes
through HostProvider.workspace.getWorkspacePaths() for the same reason.
Both resolvers degrade gracefully (undefined/false) if the host bridge is
unavailable, in which case the header builder falls back to source-derived
values.
* Add unit test as proof
---------
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
* feat(llms): include Cline free models in the cline-pass catalog
* feat(vscode): show Subscribed/Free model tabs on the ClinePass provider
* feat(cli): show Subscribed/Free sections in the ClinePass model picker
* fix(cli): drop redundant browse-all entry from ClinePass picker
* fix(cli): show only subscribed models in ClinePass onboarding picker
* feat(cli): include free models and quota explainer in ClinePass onboarding picker
* fix: shorten ClinePass free section copy
* fix(cli): strip redundant free markers from sectioned picker names
* fix: drop free from ClinePass free section copy
* fix: tighten ClinePass free section copy
* refactor: address review feedback on ClinePass free models
- single buildFeaturedModelEntries(providerId) dispatcher, builders private
- rename isClineProvider to isClineManagedProvider (includes cline-pass)
- use isClineManagedProvider in the free-model cost check
- themed tab border, pretty names on free model cards
- clearer cline-pass cost test name
* fix: address ClinePass free-model review blockers
- Stop re-sorting the cline-pass live catalog by release date in
mergeKnownModels: free models carry OpenRouter release dates, so the
sort could put a free model first and make it the fallback default
when the bundled default id rotates out of the live clinePass bucket.
Preserve the normalize-time order (pass models first) and pin it with
an end-to-end resolveProviderConfig test.
- Add the browse-all escape to the CLI ClinePass picker when the
clinePass bucket is empty (bundled fallback after a fetch failure),
so a subscriber isn't left with a free-models-only picker.
- Rename ErrorRow's local isClineManagedProvider to
isClineUsageBillingProvider: it only matches the cline provider,
unlike the shared util of the same name that also matches cline-pass.
* fix vscode f5 settings
- fixed the hot module reloading issue while debugging the extension.
- also fixed issue where deb:webview task wasn't showing as complete
* fix vscode webview dev cleanup
---------
Co-authored-by: Max Paulus 🥪 <max@cline.bot>
* perf(sdk): stop listSessions hot loop from hanging the extension host
getStateToPostToWebview rebuilt the full task history on nearly every
streaming/session event, and each rebuild ran persistence-service.listSessions,
which synchronously read + Zod-parsed every session manifest. The 10s metadata
cache meant to absorb this was wiped on every per-turn updateTaskUsage, so each
state post paid the full synchronous scan, saturating the extension-host event
loop (observed as a tight listSessions/readFileUtf8 loop in CPU profiles).
- Debounce/coalesce postStateToWebview: trailing 50ms debounce plus a single
queued follow-up so bursts collapse into one rebuild; dispose() tears it down.
- Add an async, title-only manifest reader (readSessionManifestTitle) and use it
in listSessions to resolve titles concurrently off-thread, instead of a
synchronous readFileSync + full SessionManifestSchema (Zod) parse per row. The
existing sync manifest methods are left intact.
- On single-session updates, patch just the changed record in the merged-history
cache in place instead of invalidating it, so frequent per-turn usage updates
no longer force the next state post to re-enumerate and re-merge every session.
* refactor(sdk): strengthen session history cache patching
Replace patchMetadataHistoryCacheRecord (boolean-returning, metadata-only,
no re-sort) with updateCachedSessionRecord (void, updates prompt +
metadata + updatedAt, re-sorts via shared comparator).
- Void return eliminates the ignorable fallback contract.
- Mirrors all fields the persistence layer writes (prompt, metadata,
updatedAt) so cache and disk stay consistent.
- Re-sorts after patching so the updated record bubbles to the correct
position, using a shared compareSessionHistoryRecordsByRecencyDesc
comparator also used by listHistory.
- Derives updatedAt from the HistoryItem timestamp instead of constructing
a second clock value.
- Self-invalidates on cache miss so callers never manage the fallback.
Adds tests for in-place patching, re-sorting, per-turn usage hot path,
and cache-miss invalidation.
* fix(sdk): await in-flight state post during dispose
Greptile feedback: dispose() did not await a concurrently-running
runDebouncedStatePost, so an in-flight flushStateToWebview could access
torn-down resources after disposal.
Track the runDebouncedStatePost promise in statePostInFlightPromise.
In dispose(), after setting isDisposed and clearing the timer, await
the in-flight promise (swallowing errors) before tearing down downstream
resources. The !this.isDisposed guard in the loop prevents further
iterations after disposal.
* fix(sdk): address review feedback on state-post debounce and cache patch
Three issues from code review of the listSessions hot-loop fix:
1. dispose() could await the wrong promise. A second debounced timer
firing while a flush was already running overwrote
statePostInFlightPromise with a throwaway resolved promise from the
join path, so dispose() could return while the original flush was
still executing. Extract the debounce/coalesce state machine into
StatePostDebouncer, and only track the promise from the call that
actually starts a new flush loop.
2. postStateToWebview() swallowed flush errors, resolving every pending
caller even when flushStateToWebview() threw. Callers awaiting
postStateToWebview() now see the rejection, matching pre-debounce
behavior.
3. Cache patching derived the cached updatedAt from HistoryItem.ts,
but the persistence adapter always stamps updatedAt with the
wall-clock write time. Callers like toggleTaskFavorite() reuse an
old HistoryItem whose ts predates the write, which let the cached
ordering diverge from disk until the 10s TTL expired. Stamp the
cache patch with the write time instead.
Adds unit tests for StatePostDebouncer covering the dispose race and
error-propagation regressions, and a sdk-task-history test for the
stale-updatedAt cache-ordering regression.
* fix(sdk): don't patch cache when session update write didn't land
Beatrix's review feedback: updateSession() ignored the { updated:
boolean } result from host.update() and unconditionally patched the
metadata cache. When persistence returns updated: false (session
deleted/missing, or an optimistic-concurrency retry exhausted by a
racing writer), the webview could show a fake updated record until the
cache TTL expired.
Check the write result: only patch the cache when updated === true,
otherwise invalidate it so the next read re-enumerates from disk.
---------
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
* fix(sdk/cli): emit user_id in telemetry identity attributes
Per CLINE-2406, downstream analytics expects an explicit user_id field
in authenticated SDK/CLI OpenTelemetry log attributes.
Changes:
- sdk/packages/core/src/services/telemetry/core-events.ts: add
user_id: account.id alongside the existing account_id in
identifyAccount() updateCommonProperties call.
- sdk/packages/core/src/services/telemetry/core-events.test.ts: new
identifyAccount suite verifying user_id, account_id, distinct_id, and
org context fields for authenticated user without org, with active org,
absent/blank id handling, and no-op when telemetry is undefined.
- apps/cli/src/main.ts: after loading Cline provider settings in the
runtime path, read auth.accountId and call identifyTelemetryAccount so
subsequent task.* and workspace.* events carry user_id. Document
user.extension_activated as pre-auth by design for subcommand flows.
- apps/cli/src/main.test.ts: three new tests covering saved accountId
triggers identity, missing accountId skips identity, non-Cline
provider skips identity.
* fix(sdk/cli): address review feedback on telemetry identity
- Use trimmed distinctId for user_id in identifyAccount() to keep
user_id and distinct_id consistent when IDs have whitespace
- Remove fragile type cast in CLI main.ts; ProviderSettings already
exposes auth.accountId via AuthSettingsSchema
* wip: Cline Code Desktop App
Add Bun/Tauri desktop packaging commands for macOS, Windows, and Linux, including output to dist/desktop. Enforce macOS signing and notarization requirements for shareable builds while allowing an explicit unsigned local test path.
Document desktop packaging prerequisites, ignore generated build artifacts, and wire runtime session connection updates needed by the desktop app.
Clean up and update sidecar functions.
Safe to merge as this is not a published app.
* fixes
* chat
* apply
* ClinePass support
* add build instructions and use system theme
* fix: diff status
* update tool calls display
* connection updates
* lint fix
* fix keydown
Commit 9a9300846 ("restyle chat input…", which folded in PR #12075 "replace cyan accent with new plan/act palette") deliberately rebranded the TUI accents in palette.ts:
Dark act: ANSI "cyan" → #79b8ff (and plan "yellow" → #ffea7f, success "brightGreen" → #99e89b)
Light act: #0969da → #0f72cb (and plan #9a6700 → #867100), re-derived in OKLCH to keep the same hue as the new dark accents with ≥4.5:1 contrast on white
But palette.test.ts:27-35 still asserts the old values ("preserves the existing named ANSI colors" — a test description that's now literally obsolete). So getModeAccent("act", "dark") correctly returns #79b8ff, and the test expecting "cyan" fails.
The fix is to update the two tests to the new palette values (and rename the first test, since the colors are no longer named ANSI colors).
* feat(cli): tint assistant markdown accents by the mode they were produced in
Markdown's prominent elements (headings, bold, list markers, links) were
hardcoded to the act accent. getSyntaxStyle now takes the entry's mode
and colors those elements with the matching accent -- plan segments
render yellow-tinted markdown, act segments blue -- completing the
per-mode transcript coloring. Code token colors stay constant across
modes; styles are cached per theme+mode pair. Unstamped entries follow
the current mode, same fallback as the glyph accent.
* fix(cli): resolve entry mode once for glyph and markdown accents
Address review: the accent and mode props used parallel fallback chains
that could drift; both now derive from a single resolved entryMode. Also
cover the light-theme plan/act markdown accents in tests.
* feat(cli): restyle chat input with horizontal rules and slim user bubbles
Replace the tinted-background input field with a minimal frame: full-width
horizontal rules above and below the textarea and a bold accent-colored
prompt glyph, with no background fill. User message bubbles in the
transcript match the new look: a slim neutral-gray bar with the same
glyph, no vertical padding, and no mode-colored tint.
Palette gains getInputRuleColor (neutral adaptive mid-gray for the rules)
and getUserMessageBackground (neutral bubble tint), both built on a shared
OKLAB lift helper extracted from getModeInputBackground. Home view's
robot cursor-tracking offset is adjusted for the input's new left edge,
and the spacer line between the chat input and status bar is removed.
* feat(cli): replace cyan accent with new plan/act palette
Swap the TUI accent colors: act mode goes from ANSI cyan to #79b8ff and
plan mode from ANSI yellow to #ffea7f on dark themes. Light themes get
counterparts derived in OKLCH with the same hue but darkened to hold
>=4.5:1 contrast on white (#0f72cb act, #867100 plan), selected via the
existing getModeAccent theme switch.
All hardcoded "cyan" fg literals across dialogs, model selector, config
view, and onboarding now reference palette.act so the accent is a single
source of truth. The selection highlight follows the act color. The
subliminal OKLAB chroma nudge on input backgrounds/foregrounds now leans
blue (-a, -b) instead of cyan (-a, +b) to match the new accent hue.
* feat(cli): soften success green and use act accent in markdown
Swap the success/diff green from ANSI brightGreen and #22c55e to a muted
sage #87af87 on dark themes (auto-approve banner, git diff +stats, diff
view added-sign color); light themes keep the darker #116329 for contrast
on white. Markdown prominent elements (headings, bold, list markers,
links, table headers) now use the act accent via themePalette instead of
hardcoded one-dark cyan #56b6c2.
* feat(cli): harmonize dark syntax colors with brand accent palette
Rebuild the dark syntax highlighting family around the brand anchors:
functions use the act blue, strings and inline code use the success sage,
types/italics use a dimmed plan yellow, and the remaining hues (keyword
purple, variable coral, number orange, operator ice-blue) are regenerated
in OKLCH at the same pastel lightness/chroma weight (~L 0.78, C 0.11) so
code blocks read as part of the same palette. Light theme keeps its
GitHub-light set.
* fix(cli): brighten success green to match accent palette weight
#87af87 sat at roughly half the OKLCH chroma of the act/plan anchors and
read as gray next to them; #8bd28d (L 0.80, C 0.12) matches their weight.
* fix(cli): brighten success green a step further (#99e89b)
* feat(cli): polish status bar usage display and ClinePass model name
- Cost always renders with two decimals ($0.00) instead of switching to
four decimals under a cent; the turn summary line drops its three-decimal
format for the same reason.
- Token count next to the context bar is now just the number; the word
'tokens' was redundant with the bar right beside it.
- Context window bar shrinks from 8 to 6 cells.
- ClinePass models resolve their friendly models.dev name like every other
provider and get a (ClinePass) suffix: 'GLM 5.2 (ClinePass)' instead of
'ClinePass/glm-5.2'.
- ClinePass no longer shows '$0.00 (included with subscription)' -- cost is
simply hidden for subscription providers.
* fix(cli): place ClinePass suffix after reasoning effort in model name
* fix(cli): format ClinePass model name as 'ClinePass: <model>' prefix
* feat(cli): color transcript entries by the mode they were produced in (#12083)
* feat(cli): color transcript entries by the mode they were produced in
Previously the whole transcript retinted to the current mode's accent on
every plan/act toggle. Entries now record the agent mode active when
they were produced and keep that accent permanently, so a session reads
as a visible history of plan (yellow) and act (blue) segments.
How the mode is captured:
- Live sessions: appendEntry in SessionProvider stamps entries from a
uiMode ref, covering every creation site including mid-run
switch_to_act_mode flips (which already call setUiMode through the
runtime dialog bridge).
- Resumed sessions: hydrateSessionMessages recovers the mode from the
persisted <user_input mode="..."> wrappers via a new shared
parseUserInputMode helper, and flips to act at switch_to_act_mode tool
calls. Transcripts without wrappers stay unstamped and keep the
current-mode fallback accent, matching the old behavior.
- Restores: the /history resume and checkpoint-restore paths insert
hydrated history via replaceEntries instead of appendEntry loops, so
live-entry stamping cannot overwrite hydration's stamps (which would
lock resumed transcripts to the resume-time accent).
The load-bearing core fix: readPersistedMessagesFile stripped the
user_input wrappers and mode notices from user text on every read
('display sanitization'). That read path also feeds session restarts
(mode toggle, compaction-mode change, model change, fork, recovery),
which re-persist what they read -- so every restart laundered the mode
markers off disk and out of the model's seeded context, leaving nothing
for hydration to recover. Reads now return persisted messages verbatim
and formatting is the display surface's job: the CLI TUI, history
titles, and the VS Code SDK history loader already formatted at their
boundaries; the cline-hub webview history mapping and the CLI HTML
export (which used normalizeUserInput and leaked mode_notice text) now
do too. Connectors only surface assistant text, and the remaining
readMessages consumers are programmatic (usage math, re-seeding,
compaction input) where raw is correct.
* fix(shared): match parseUserInputMode exactly to what the writer emits
Drop the case-insensitive flag and the 'zen' value from the wrapper
regex: formatUserInputBlock only ever writes lowercase act/plan/yolo, so
anything else the parser accepted (uppercase look-alikes in adversarial
content, a zen value with no writer) could never be real persisted data.
* feat(cli): restyle chat input with horizontal rules and slim user bubbles
Replace the tinted-background input field with a minimal frame: full-width
horizontal rules above and below the textarea and a bold accent-colored
prompt glyph, with no background fill. User message bubbles in the
transcript match the new look: a slim neutral-gray bar with the same
glyph, no vertical padding, and no mode-colored tint.
Palette gains getInputRuleColor (neutral adaptive mid-gray for the rules)
and getUserMessageBackground (neutral bubble tint), both built on a shared
OKLAB lift helper extracted from getModeInputBackground. Home view's
robot cursor-tracking offset is adjusted for the input's new left edge,
and the spacer line between the chat input and status bar is removed.
* feat(cli): replace cyan accent with new plan/act palette (#12075)
* feat(cli): replace cyan accent with new plan/act palette
Swap the TUI accent colors: act mode goes from ANSI cyan to #79b8ff and
plan mode from ANSI yellow to #ffea7f on dark themes. Light themes get
counterparts derived in OKLCH with the same hue but darkened to hold
>=4.5:1 contrast on white (#0f72cb act, #867100 plan), selected via the
existing getModeAccent theme switch.
All hardcoded "cyan" fg literals across dialogs, model selector, config
view, and onboarding now reference palette.act so the accent is a single
source of truth. The selection highlight follows the act color. The
subliminal OKLAB chroma nudge on input backgrounds/foregrounds now leans
blue (-a, -b) instead of cyan (-a, +b) to match the new accent hue.
* feat(cli): soften success green and use act accent in markdown
Swap the success/diff green from ANSI brightGreen and #22c55e to a muted
sage #87af87 on dark themes (auto-approve banner, git diff +stats, diff
view added-sign color); light themes keep the darker #116329 for contrast
on white. Markdown prominent elements (headings, bold, list markers,
links, table headers) now use the act accent via themePalette instead of
hardcoded one-dark cyan #56b6c2.
* feat(cli): harmonize dark syntax colors with brand accent palette
Rebuild the dark syntax highlighting family around the brand anchors:
functions use the act blue, strings and inline code use the success sage,
types/italics use a dimmed plan yellow, and the remaining hues (keyword
purple, variable coral, number orange, operator ice-blue) are regenerated
in OKLCH at the same pastel lightness/chroma weight (~L 0.78, C 0.11) so
code blocks read as part of the same palette. Light theme keeps its
GitHub-light set.
* fix(cli): brighten success green to match accent palette weight
#87af87 sat at roughly half the OKLCH chroma of the act/plan anchors and
read as gray next to them; #8bd28d (L 0.80, C 0.12) matches their weight.
* fix(cli): brighten success green a step further (#99e89b)
* fix(shared): stop deleting mode_notice from outbound prompts
The mode-switch notice from #12057 never reached the model:
prepareTurnInput sanitizes every outbound prompt with normalizeUserInput
before wrapping it, and #12057 put the mode_notice strip inside
normalizeUserInput -- so the host deleted the notice on every send. The
transcript confirms it: messages sent after a toggle carry the
user_input wrapper but no notice, and models asked about it confabulate
having seen one because the system prompt describes the tag.
Move the strip into a dedicated stripModeNotices() applied only at
display boundaries: formatDisplayUserInput (TUI hydration, title
inference), deriveTitleFromPrompt (session titles), and the TUI queued
prompt echo. normalizeUserInput now preserves notices, with a
regression test pinning the outbound behavior. Side benefit: notices
survive the message-builder history normalization and the pending
prompt queue, so queued sends deliver them too.
* docs(shared): correct formatModeSwitchNotice JSDoc after strip relocation
* refactor(shared): generalize notice stripping to stripTagElements
stripModeNotices becomes a thin policy wrapper (DISPLAY_HIDDEN_TAGS owns
the what-to-hide list in one place) over a generic stripTagElements that
removes whole elements for any tag list -- the remove-element counterpart
to xmlTagsRemoval. Call sites at display boundaries now carry comments
explaining why stripping happens there and not in normalizeUserInput,
which also sanitizes model-bound prompts.
* revert(shared): drop stripTagElements generalization, keep simple stripModeNotices
The generic tag stripper added API surface without a second use case;
stripModeNotices goes back to the direct implementation. The display-vs-
model call-site comments from the same commit stay.
* feat(cli): make plan/act mode switches visible to the model
The mode signal already rides on every user message via the
<user_input mode="..."> wrapper, but nothing ever told the model what
the attribute means, and a manual plan/act toggle produced no inline
signal at all -- only an invisible system prompt swap the model cannot
diff. Two additions:
- The CLI system prompt now explains the mode attribute (both modes,
since after a switch the transcript still contains messages tagged
with the other mode) and that the newest message's mode governs.
- A user-initiated toggle stamps the next user message with a
<mode_notice> block marking the switch, e.g. "The user switched from
act mode to plan mode before sending this message." Round trips that
return to the mode the model last saw cancel out. The model-initiated
switch_to_act_mode path is excluded: its continuation prompt already
announces the switch.
The notice vocabulary lives in @cline/shared next to the user_input
wrapper it extends, and normalizeUserInput hides the whole element from
transcript display the same structural way it strips the wrapper tags.
* fix(shared): strip mode_notice elements without polynomial regex
CodeQL flagged the lazy dot-all pattern (js/polynomial-redos): with the
global flag, every unmatched opening tag re-scans to the end of the
string, which is quadratic on adversarial transcript content. Replace
it with an indexOf-based splice that removes matched elements in linear
time and leaves unclosed tags intact, with a regression test on 50k
repeated open tags.
restartWithMessages cleared startupPromise and tore down the active
session before the replacement registered, leaving a window with no
active session and no startup in flight. A message submitted in that
window (e.g. typed right after a plan/act Tab toggle) made ensureReady
boot a blank fresh session, which then won the active slot over the
restarted session carrying the conversation history -- the model
responded as if the conversation had just started.
Publish the restart itself as the in-flight startupPromise so any
concurrent ensureReady waits for the restart instead of booting an
empty session. The barrier is cleared once the restart settles,
keeping failed restarts retryable by the next ensureReady.
* fix(cli): end plan-mode run on switch_to_act_mode and auto-continue with act tools
The CLI's switch_to_act_mode tool only queued the mode change; it was
applied after the whole turn finished. The model kept running the rest
of the turn with plan-mode tools (no editor) despite the tool result
claiming it now had edit access, so it fell back to editing files via
run_commands (sed/heredocs).
Mirror the VS Code extension's approach: the switch tool now completes
the run (lifecycle.completesRun), the pending mode change rebuilds the
session with act-mode tools, and a canned continuation prompt resumes
the approved plan automatically. Pending mode changes are tagged with
their source (tool vs UI toggle) so a Tab toggle racing a natural turn
completion can never auto-start plan execution the user did not
approve. The synthetic continuation prompt is hidden from transcript
hydration, and the plan-mode prompt/tool description now warn that
switching immediately starts execution.
* refactor(cli): show act-mode continuation prompt on resume instead of filtering it
Displaying the synthetic user message honestly beats exact-string
matching at the display layer, which was brittle and did not cover
other transcript consumers anyway. The live TUI still never echoes it;
it only appears as a user bubble when resuming a session. A
synthetic-message marker plumbed through SendSessionInput is the
principled follow-up if hiding it becomes worth the SDK surface
change.
* Revert "refactor(cli): show act-mode continuation prompt on resume instead of filtering it"
This reverts commit 969a24f9c9.
* fix(hub): hydrate tool results in session message mapping
Map historical tool call/use and tool result blocks into webview tool events, including same-message results and following user result messages. Add tests to verify hydrated outputs and block ordering so restored sessions render completed tool interactions correctly.
* feedback
#11986 (Forcefully enable ClinePass on the CLI) hardcoded isClinePassEnabled: true in session-runtime.ts, provider-catalog.ts, and main.ts and removed the ext-cline-pass feature-flag check, but left the corresponding tests asserting the old flag-driven / disabled behavior. They fail on main (and every branch that merges it).
- session-runtime.test.ts: expect getLastUsedProviderSettings called with isClinePassEnabled: true.
- provider-catalog.test.ts: drop the obsolete getBooleanFlagEnabled('ext-cline-pass') assertion (source no longer reads the flag) and its now-unused mock; keep the isClinePassEnabled: true expectation.
- main.test.ts: the ClinePass flag is no longer read during startup, so getBooleanFlagEnabled is never called. Re-target the 'seed identity before flags' ordering assertion at refreshCliFeatureFlagsInBackground (which is still invoked after seeding), and wire that mock through featureFlagMocks.
The vscode test (Extension Integration Tests) job is red on main: the updateAutoApprovalSettings suite (added in #11929) is vitest-native but was being collected and run by the Mocha @vscode/test-cli runner, where vitest-only matchers (toHaveBeenCalledWith / toHaveBeenCalledOnce / not.toHaveBeenCalled) are not registered, failing with 'is not a function'.
build-tests.js already excludes bun:test-owned tests from the Node-based out/ tree (single source of truth for the runner split). Extend that same mechanism to also exclude vitest-owned tests (files importing from 'vitest'), so neither bun nor vitest suites are ever compiled into the mocha out/ tree. Verified locally: detection catches the state suite (123 non-mocha test files total) while preserving the existing 60 bun __tests__ exclusions. No coverage lost — these suites run under test:vitest / bun.