Skip to content

feat(perf): progressive MCP availability — MCP no longer blocks first input - #3994

Merged
wenshao merged 8 commits into
mainfrom
feat/first-screen-performance-optimization
May 13, 2026
Merged

feat(perf): progressive MCP availability — MCP no longer blocks first input#3994
wenshao merged 8 commits into
mainfrom
feat/first-screen-performance-optimization

Conversation

@chiga0

@chiga0 chiga0 commented May 9, 2026

Copy link
Copy Markdown
Collaborator

Why

Config.initialize() currently runs MCP discovery synchronously, so the cli can't accept user input until every configured MCP server finishes its discover handshake. One slow or hung server bottlenecks every user who has MCP configured.

Measured TTI (time to first prompt input) before this PR:

Scenario TTI
No MCP 480 ms
1 fast MCP 875 ms
2 fast + 1 slow MCP (5 s/req) 7.1 s
1 hung MCP server 10.5 s

(macOS arm64 / Node 24.15, n=30/fixture, p50, profiler enabled.)

What changes

Progressive MCP availability: Config.initialize() returns as soon as built-in tools are ready, and MCP discovery runs in a fire-and-forget background path. Each server's tools land in the registry as it becomes ready, and the cli debounces setTools() calls into one-frame (16 ms) batches so the model sees the consolidated tool list shortly after each server settles.

Interactive vs non-interactive split (important — see the "behavioral audit" section below):

  • Interactive cli (qwen-code with a TTY): Config.initialize() returns fast, UI appears immediately, MCP tools come online progressively. setTools() fires at most once per ~16 ms frame.
  • Non-interactive paths (--prompt, stream-json, ACP): Config.initialize() returns fast, then the path explicitly awaits Config.waitForMcpReady() BEFORE the first model send. Same tool surface as the legacy synchronous behavior — no silent regression for CI / scripts / IDE integrations.

17 files, +1089 / -68:

File Change Type
packages/core/src/config/config.ts Config.initialize() skipDiscovery + new startMcpDiscoveryInBackground() + new waitForMcpReady() (resolves when background discovery settles, no-op when nothing was started). MCPServerConfig.discoveryTimeoutMs (last positional ctor param). Behavior
packages/core/src/tools/tool-registry.ts New getMcpClientManager() getter. Behavior
packages/core/src/tools/mcp-client-manager.ts discoverAllMcpToolsIncremental emits mcp-client-update after IN_PROGRESS / COMPLETED. Per-server discover wrapped in timeout (stdio 30 s, remote 5 s). Behavior
packages/cli/src/gemini.tsx Non-interactive path now awaits config.waitForMcpReady() before runNonInteractive. Profiler sink registration + first_paint checkpoint + setInteractiveMode(true). Behavior + measurement
packages/cli/src/nonInteractive/session.ts Stream-json Session.initialize() awaits waitForMcpReady() before resolving. Behavior
packages/cli/src/acp-integration/acpAgent.ts ACP top-level + per-session paths await waitForMcpReady(). Behavior
packages/cli/src/ui/AppContainer.tsx New useEffect (gated on isConfigInitialized) for 16 ms batch-flush of setTools() + deferred startup-profile finalize (waits for MCP settle or 35 s cap). Behavior + measurement
packages/core/src/utils/startupEventSink.ts (new) Cross-package sink so core can emit profiler events without reverse-depending on cli. No-op when no sink registered. Measurement
packages/cli/src/utils/startupProfiler.ts Extended with events, recordStartupEvent, setInteractiveMode, derivedPhases, heap snapshots, MAX_EVENTS cap, QWEN_CODE_PROFILE_STARTUP_OUTER / _NO_HEAP env opt-ins. Measurement
packages/core/src/{config/config.ts, core/client.ts, tools/mcp-client-manager.ts} Emit profiler events at key checkpoints (tool_registry_created, gemini_tools_updated, mcp_discovery_start, mcp_server_ready:<name>, mcp_first_tool_registered, mcp_all_servers_settled). Measurement
packages/core/src/index.ts Export sink interface. Measurement
docs/users/configuration/settings.md Document the four new env vars: QWEN_CODE_LEGACY_MCP_BLOCKING, QWEN_CODE_PROFILE_STARTUP_OUTER, QWEN_CODE_PROFILE_STARTUP_NO_HEAP + clarify QWEN_CODE_PROFILE_STARTUP. Docs
docs/users/features/mcp.md New "Progressive availability and discovery timeouts" section explaining the behavior, discoveryTimeoutMs override syntax, and the rollback escape hatch. Docs

Rollback: QWEN_CODE_LEGACY_MCP_BLOCKING=1 restores the previous synchronous semantics. Kept ≥ 1 release as an escape hatch. Single-commit revert otherwise.

Profiler zero-cost when off: every profiler entry point short-circuits in a single null/flag check when QWEN_CODE_PROFILE_STARTUP is unset. Heisenberg overhead measured at -1.12 % Δp50 vs profile-off (Welch p = 0.092, n=30 × 3 configs) — within statistical noise.

Measured results (after this PR, same fixtures)

Fixture TTI before → after Δ p50 Welch's t-test p
no-mcp 480 → 472 ms -1.6% (noise) n/a
1 fast MCP 875 → 472 ms -46% < 1e-9
2 fast + 1 slow 7101 → 471 ms -93% < 1e-9
1 hung MCP 10483 → 490 ms -95% < 1e-9

Lag from "first MCP server ready" → "model sees updated tool list" (2 fast + 1 slow): 6235 ms → 17.1 ms — confirms the 16 ms batch window is the operative cap.

mcp_all_servers_settled is unchanged — slow servers still take their time, but the work is now invisible to interactive users (and non-interactive paths still wait for them, by design).

first_paint is unchanged (±3 % noise) across all fixtures, confirming this PR doesn't touch the pre-mount path.

How to validate

The profiler instrumentation in this PR is the verification layer: anyone can reproduce the numbers above on their own machine.

One-off run (no MCP setup required)

# Build the bundle once
npm run bundle

# Run with profiler enabled (SANDBOX=1 satisfies the profiler's
# sandbox-child gate — see packages/cli/src/utils/startupProfiler.ts).
# The cli enters interactive mode; type anything and press Ctrl+C.
mkdir -p /tmp/qwen-perf-test/.qwen
QWEN_CODE_PROFILE_STARTUP=1 SANDBOX=1 \
  QWEN_HOME=/tmp/qwen-perf-test/.qwen HOME=/tmp/qwen-perf-test \
  node dist/cli.js

# The profile JSON lands here. Pretty-print the derived phases:
cat /tmp/qwen-perf-test/.qwen/startup-perf/*.json | jq .derivedPhases

Expected derivedPhases keys when run with the changes in this PR:

  • module_load — Node process start → main_entry checkpoint
  • to_first_paint — Ink first frame
  • config_initialize_durconfig.initialize() wall time
  • to_input_enabledTTI, what users feel
  • mcp_first_tool / mcp_all_settled — only present when MCP configured
  • gemini_tools_lagmcp_first_tool → first setTools() after it (one frame under PR)

Before / after comparison (with MCP)

Run twice — once on main, once on this branch — same fixture, compare derivedPhases.to_input_enabled:

# 1. Set up a fixture with the MCP shape you want to measure
mkdir -p /tmp/qwen-mcp-test/.qwen
cat > /tmp/qwen-mcp-test/.qwen/settings.json <<'JSON'
{
  "mcpServers": {
    "fast":  { "command": "node", "args": ["/tmp/echo-mcp.mjs"], "env": { "ECHO_MCP_NAME": "fast" } },
    "slow":  { "command": "node", "args": ["/tmp/echo-mcp.mjs"], "env": { "ECHO_MCP_NAME": "slow", "ECHO_MCP_DELAY_MS": "1500" } }
  }
}
JSON
# A 50-line stdio MCP echo server (initialize/tools-list/prompts-list/resources-list)
# is enough; happy to share the script in PR comments if reviewers want it verbatim.

# 2. main HEAD
git checkout main && npm run bundle
rm -f /tmp/qwen-mcp-test/.qwen/startup-perf/*.json
QWEN_CODE_PROFILE_STARTUP=1 SANDBOX=1 \
  QWEN_HOME=/tmp/qwen-mcp-test/.qwen HOME=/tmp/qwen-mcp-test \
  node dist/cli.js   # type/Ctrl+C
mv /tmp/qwen-mcp-test/.qwen/startup-perf/*.json /tmp/before.json

# 3. this PR
git checkout feat/first-screen-performance-optimization && npm run bundle
rm -f /tmp/qwen-mcp-test/.qwen/startup-perf/*.json
QWEN_CODE_PROFILE_STARTUP=1 SANDBOX=1 \
  QWEN_HOME=/tmp/qwen-mcp-test/.qwen HOME=/tmp/qwen-mcp-test \
  node dist/cli.js
mv /tmp/qwen-mcp-test/.qwen/startup-perf/*.json /tmp/after.json

# 4. Compare
jq -r '"TTI=\(.derivedPhases.to_input_enabled)ms  config_init=\(.derivedPhases.config_initialize_dur)ms  gemini_tools_lag=\(.derivedPhases.gemini_tools_lag)ms"' /tmp/before.json /tmp/after.json

The structured benchmark harness I used to generate the numbers above (Welch's t-test + 4 fixtures × 30 runs + node-pty interactive driver) isn't in this PR. Happy to share it as a separate followup tooling PR if reviewers want a reproducible CI gate.

Behavioral audit

Config.initialize() now returns BEFORE MCP discovery completes. Mitigations in this PR:

  • Interactive cli: AppContainer subscribes to mcp-client-update and refreshes setTools() as servers come online. Model sees new tools within ~16 ms of each server settling.
  • Non-interactive --prompt (gemini.tsx:746-752): awaits config.waitForMcpReady() before the first model send.
  • Stream-json sessions (nonInteractive/session.ts:138-148): awaits waitForMcpReady() inside Session.initialize() before the first prompt can be dispatched.
  • ACP agent (acp-integration/acpAgent.ts:86, 680): awaits waitForMcpReady() after both Config.initialize() call sites.
  • MCPDiscoveryState.COMPLETED still transitions exactly once per discovery cycle (verified by new regression test that checks mcp-client-update emit ordering).

Things reviewers should double-check:

  • Any extension / plugin that reads config.getToolRegistry() immediately after config.initialize() resolves and assumes MCP tools are present.
  • Test fixtures that mock Config.initialize() and expect specific discoveryAllTools call counts (this PR's Config.initialize no longer calls discoverAllTools synchronously by default — see new config.test.ts cases).

Test plan

  • packages/core/src/config/config.test.ts136 tests (132 existing + 4 new for skipInlineMcpDiscovery default, QWEN_CODE_LEGACY_MCP_BLOCKING=1 escape hatch, waitForMcpReady no-op when no discovery started)
  • packages/core/src/tools/mcp-client-manager.test.ts12 tests (10 existing + 2 new for per-server discoveryTimeoutMs enforcement and IN_PROGRESS → COMPLETED mcp-client-update emit ordering)
  • packages/core/src/core/client.test.ts — 92 tests
  • packages/cli/src/utils/startupProfiler.test.ts — 18 tests (11 existing + 7 new for events / OUTER / heap / derivedPhases)
  • packages/core/src/utils/startupEventSink.test.ts — 4 tests (no-op / forward / exception isolation / null reset)
  • tsc --noEmit clean for packages/core and packages/cli
  • eslint clean on touched files
  • Total: 318 tests passing in the impacted suites (up from 313)

Out of scope (future PRs)

  • PR-B — startup main path optimization: loadSettingsAsync + initializeApp parallelization + dynamic import of the interactive UI (drop ~200-500 KB Ink + AppContainer from non-interactive / headless / ACP / subcommand bundles) + module-eval-time prefetch. Estimated additional TTI improvement: ~100-150 ms across all users + significant bundle reduction for headless paths.
  • Runtime MCP refresh / reload incremental path (extends this PR's progressive model to /reload-plugins, ExtensionManager.refreshTools(), list-changed events).
  • MCPServerConfig.discoveryTimeoutMs exposed via JSON schema + settings dialog so users can tune per-server.
  • Nightly perf-regression CI on the startup harness.

🤖 Generated with Qwen Code

@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Summary

Package Lines Statements Functions Branches
CLI 75.72% 75.72% 76.97% 80.48%
Core 78.4% 78.4% 81.01% 82.66%
CLI Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   75.72 |    80.48 |   76.97 |   75.72 |                   
 src               |   73.49 |    66.66 |   76.47 |   73.49 |                   
  gemini.tsx       |   61.04 |    57.29 |   66.66 |   61.04 | ...96,913-916,924 
  ...ractiveCli.ts |   80.02 |    68.61 |   78.57 |   80.02 | ...1021,1059,1162 
  ...liCommands.ts |   76.17 |    73.33 |     100 |   76.17 | ...50-274,299,401 
  ...ActiveAuth.ts |     100 |     87.5 |     100 |     100 | 66-80             
 ...cp-integration |   54.45 |    66.34 |   58.82 |   54.45 |                   
  acpAgent.ts      |   56.74 |    66.66 |   65.51 |   56.74 | ...12-914,928-936 
  authMethods.ts   |   12.19 |      100 |       0 |   12.19 | 11-31,34-38,41-50 
  errorCodes.ts    |       0 |        0 |       0 |       0 | 1-22              
  ...DirContext.ts |     100 |      100 |     100 |     100 |                   
 ...ration/service |   68.65 |    83.33 |   66.66 |   68.65 |                   
  filesystem.ts    |   68.65 |    83.33 |   66.66 |   68.65 | ...32,77-94,97-98 
 ...ration/session |   76.02 |    70.59 |      84 |   76.02 |                   
  ...ryReplayer.ts |   65.93 |    75.67 |   81.81 |   65.93 | ...40-255,268-269 
  Session.ts       |   75.12 |    68.89 |    85.1 |   75.12 | ...2456,2462-2465 
  ...entTracker.ts |   90.85 |    84.84 |      90 |   90.85 | ...35,199,251-260 
  index.ts         |       0 |        0 |       0 |       0 | 1-40              
  ...ssionUtils.ts |   84.21 |    77.77 |     100 |   84.21 | ...37-153,209-211 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...ssion/emitters |   96.01 |    90.75 |    92.3 |   96.01 |                   
  BaseEmitter.ts   |   76.92 |    66.66 |      80 |   76.92 | 23-24,39-40,55-56 
  ...ageEmitter.ts |     100 |    89.47 |     100 |     100 | 109,111           
  PlanEmitter.ts   |     100 |      100 |     100 |     100 |                   
  ...allEmitter.ts |   98.06 |     92.3 |     100 |   98.06 | 227-228,327,335   
  index.ts         |       0 |        0 |       0 |       0 | 1-10              
 ...ession/rewrite |   90.36 |    87.83 |   94.11 |   90.36 |                   
  LlmRewriter.ts   |      81 |       84 |     100 |      81 | ...,88-89,155-159 
  ...Middleware.ts |   95.83 |    85.71 |     100 |   95.83 | 119,127-129       
  TurnBuffer.ts    |     100 |      100 |     100 |     100 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 src/auth          |   97.68 |    94.85 |   95.45 |   97.68 |                   
  allProviders.ts  |     100 |      100 |     100 |     100 |                   
  ...iderConfig.ts |    97.6 |    95.04 |     100 |    97.6 | ...61,411,433-434 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 src/auth/install  |   98.57 |    88.88 |     100 |   98.57 |                   
  ...nstallPlan.ts |   98.57 |    88.88 |     100 |   98.57 | 80,93             
 ...viders/alibaba |   96.96 |    66.66 |   66.66 |   96.96 |                   
  ...baStandard.ts |     100 |      100 |     100 |     100 |                   
  codingPlan.ts    |   93.67 |    66.66 |   66.66 |   93.67 | 83,87-89,94       
  tokenPlan.ts     |     100 |      100 |     100 |     100 |                   
 ...oviders/custom |     100 |      100 |     100 |     100 |                   
  ...omProvider.ts |     100 |      100 |     100 |     100 |                   
 ...roviders/oauth |    91.5 |    77.03 |   97.05 |    91.5 |                   
  openrouter.ts    |   84.37 |    33.33 |     100 |   84.37 | 43-48             
  ...outerOAuth.ts |    91.9 |    79.06 |   96.87 |    91.9 | ...53-655,699-701 
 ...ers/thirdParty |     100 |      100 |     100 |     100 |                   
  deepseek.ts      |     100 |      100 |     100 |     100 |                   
  idealab.ts       |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  zai.ts           |     100 |      100 |     100 |     100 |                   
 src/commands      |   59.83 |    85.71 |   43.47 |   59.83 |                   
  auth.ts          |     100 |    83.33 |     100 |     100 | 11,14             
  channel.ts       |   56.66 |      100 |       0 |   56.66 | 15-19,27-34       
  extensions.tsx   |   96.55 |      100 |      50 |   96.55 | 37                
  hooks.tsx        |   66.66 |      100 |       0 |   66.66 | 20-24             
  mcp.ts           |   94.73 |      100 |      50 |   94.73 | 28                
  review.ts        |   51.85 |      100 |       0 |   51.85 | 24-35,38          
  serve.ts         |   11.84 |      100 |       0 |   11.84 | ...5,44-85,87-123 
 ...mmands/channel |   39.25 |    79.45 |      50 |   39.25 |                   
  ...l-registry.ts |    8.57 |      100 |       0 |    8.57 | 6-21,24-42        
  config-utils.ts  |      92 |      100 |   66.66 |      92 | 21-26             
  configure.ts     |    14.7 |      100 |       0 |    14.7 | 18-21,23-84       
  pairing.ts       |   26.31 |      100 |       0 |   26.31 | ...30,40-50,52-65 
  pidfile.ts       |   96.34 |    86.95 |     100 |   96.34 | 49,59,91          
  start.ts         |   30.98 |       52 |   69.23 |   30.98 | ...72-475,484-486 
  status.ts        |   17.85 |      100 |       0 |   17.85 | 15-26,32-76       
  stop.ts          |      20 |      100 |       0 |      20 | 14-48             
 ...nds/extensions |   84.53 |    88.95 |   81.81 |   84.53 |                   
  consent.ts       |   71.65 |    89.28 |   42.85 |   71.65 | ...85-141,156-162 
  disable.ts       |     100 |      100 |     100 |     100 |                   
  enable.ts        |     100 |      100 |     100 |     100 |                   
  install.ts       |    75.6 |    66.66 |   66.66 |    75.6 | ...39-142,145-153 
  link.ts          |     100 |      100 |     100 |     100 |                   
  list.ts          |     100 |      100 |     100 |     100 |                   
  new.ts           |     100 |      100 |     100 |     100 |                   
  settings.ts      |   99.15 |      100 |   83.33 |   99.15 | 151               
  uninstall.ts     |    37.5 |      100 |   33.33 |    37.5 | 23-45,57-64,67-70 
  update.ts        |   96.32 |      100 |     100 |   96.32 | 101-105           
  utils.ts         |   60.24 |    28.57 |     100 |   60.24 | ...81,83-87,89-93 
 ...les/mcp-server |       0 |        0 |       0 |       0 |                   
  example.ts       |       0 |        0 |       0 |       0 | 1-60              
 src/commands/mcp  |   92.29 |    86.08 |   88.88 |   92.29 |                   
  add.ts           |     100 |    98.03 |     100 |     100 | 293               
  list.ts          |   91.22 |    80.76 |      80 |   91.22 | ...19-121,146-147 
  reconnect.ts     |   76.72 |    71.42 |   85.71 |   76.72 | 35-48,153-175     
  remove.ts        |     100 |       80 |     100 |     100 | 21-25             
 ...ommands/review |   11.57 |      100 |       0 |   11.57 |                   
  cleanup.ts       |   17.94 |      100 |       0 |   17.94 | ...01-106,108-109 
  deterministic.ts |   13.75 |      100 |       0 |   13.75 | ...22-738,740-741 
  fetch-pr.ts      |   11.36 |      100 |       0 |   11.36 | ...80-201,203-204 
  load-rules.ts    |   11.32 |      100 |       0 |   11.32 | ...41-153,155-156 
  pr-context.ts    |    6.22 |      100 |       0 |    6.22 | ...97-312,314-315 
  presubmit.ts     |    9.35 |      100 |       0 |    9.35 | ...62-287,289-290 
 ...nds/review/lib |      30 |      100 |       0 |      30 |                   
  gh.ts            |   22.58 |      100 |       0 |   22.58 | ...49,53-54,62-69 
  git.ts           |   22.72 |      100 |       0 |   22.72 | 15-18,29-39,43-44 
  paths.ts         |   52.94 |      100 |       0 |   52.94 | ...26,37-38,42-43 
 src/config        |   92.72 |    85.31 |   85.71 |   92.72 |                   
  auth.ts          |   86.98 |    80.32 |     100 |   86.98 | ...26-227,243-244 
  config.ts        |   88.32 |    85.52 |      76 |   88.32 | ...1719,1743-1744 
  keyBindings.ts   |   96.11 |       50 |     100 |   96.11 | 169-172           
  ...idersScope.ts |      92 |       90 |     100 |      92 | 11-12             
  sandboxConfig.ts |    58.9 |    61.53 |   66.66 |    58.9 | ...54-68,73,77-89 
  settings.ts      |   85.51 |    87.19 |   86.48 |   85.51 | ...1148,1153-1156 
  ...ingsSchema.ts |     100 |      100 |     100 |     100 |                   
  ...tedFolders.ts |   96.22 |       94 |     100 |   96.22 | ...88-190,205-206 
 ...nfig/migration |   94.89 |    78.94 |   83.33 |   94.89 |                   
  index.ts         |   94.87 |    88.88 |     100 |   94.87 | 91-92             
  scheduler.ts     |   96.55 |    77.77 |     100 |   96.55 | 19-20             
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...ation/versions |   94.74 |       96 |     100 |   94.74 |                   
  ...-v2-shared.ts |     100 |      100 |     100 |     100 |                   
  v1-to-v2.ts      |   81.75 |    90.19 |     100 |   81.75 | ...28-229,231-247 
  v2-to-v3.ts      |     100 |      100 |     100 |     100 |                   
  v3-to-v4.ts      |     100 |      100 |     100 |     100 |                   
 src/core          |     100 |      100 |     100 |     100 |                   
  auth.ts          |     100 |      100 |     100 |     100 |                   
  initializer.ts   |     100 |      100 |     100 |     100 |                   
  theme.ts         |     100 |      100 |     100 |     100 |                   
 src/dualOutput    |   63.09 |    64.51 |   55.55 |   63.09 |                   
  ...tputBridge.ts |   62.94 |    65.51 |   56.25 |   62.94 | ...22-323,331-334 
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/export        |       0 |        0 |       0 |       0 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-7               
 src/generated     |     100 |      100 |     100 |     100 |                   
  git-commit.ts    |     100 |      100 |     100 |     100 |                   
 src/i18n          |   84.88 |    78.88 |   66.66 |   84.88 |                   
  index.ts         |   70.44 |       74 |   53.84 |   70.44 | ...71-272,282-287 
  languages.ts     |   95.33 |    86.48 |     100 |   95.33 | ...67,195-198,213 
  ...nslateKeys.ts |     100 |      100 |     100 |     100 |                   
  ...lationDict.ts |   93.33 |    66.66 |     100 |   93.33 | 15                
 src/i18n/locales  |     100 |      100 |     100 |     100 |                   
  ca.js            |     100 |      100 |     100 |     100 |                   
  de.js            |     100 |      100 |     100 |     100 |                   
  en.js            |     100 |      100 |     100 |     100 |                   
  fr.js            |     100 |      100 |     100 |     100 |                   
  ja.js            |     100 |      100 |     100 |     100 |                   
  pt.js            |     100 |      100 |     100 |     100 |                   
  ru.js            |     100 |      100 |     100 |     100 |                   
  zh-TW.js         |     100 |      100 |     100 |     100 |                   
  zh.js            |     100 |      100 |     100 |     100 |                   
 ...nonInteractive |   72.57 |    71.12 |   74.07 |   72.57 |                   
  session.ts       |   76.64 |     69.4 |   85.71 |   76.64 | ...23-824,833-843 
  types.ts         |    42.5 |      100 |   33.33 |    42.5 | ...80-581,584-585 
 ...active/control |   77.04 |    88.23 |      80 |   77.04 |                   
  ...rolContext.ts |    7.14 |        0 |       0 |    7.14 | 49-84             
  ...Dispatcher.ts |   91.66 |    91.83 |   88.88 |   91.66 | ...54-372,388,391 
  ...rolService.ts |       8 |        0 |       0 |       8 | 46-179            
 ...ol/controllers |    7.04 |       80 |   13.33 |    7.04 |                   
  ...Controller.ts |   19.32 |      100 |      60 |   19.32 | 81-118,127-210    
  ...Controller.ts |       0 |        0 |       0 |       0 | 1-56              
  ...Controller.ts |    3.96 |      100 |   11.11 |    3.96 | ...61-379,389-494 
  ...Controller.ts |   14.06 |      100 |       0 |   14.06 | ...82-117,130-133 
  ...Controller.ts |     5.2 |      100 |       0 |     5.2 | ...21-433,442-472 
 .../control/types |       0 |        0 |       0 |       0 |                   
  serviceAPIs.ts   |       0 |        0 |       0 |       0 | 1                 
 ...Interactive/io |   97.98 |    93.72 |   95.18 |   97.98 |                   
  ...putAdapter.ts |   97.89 |    92.82 |   98.07 |   97.89 | ...1303,1398-1399 
  ...putAdapter.ts |      96 |    91.66 |   85.71 |      96 | 51-52             
  ...nputReader.ts |     100 |    94.73 |     100 |     100 | 67                
  ...putAdapter.ts |   98.28 |      100 |      90 |   98.28 | 81-82,122-123     
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/patches       |       0 |        0 |       0 |       0 |                   
  is-in-ci.ts      |       0 |        0 |       0 |       0 | 1-17              
 src/remoteInput   |   86.98 |       75 |   85.71 |   86.98 |                   
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  ...putWatcher.ts |   88.12 |    76.08 |   91.66 |   88.12 | ...21-222,233-236 
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/serve         |   80.24 |    80.11 |   87.91 |   80.24 |                   
  auth.ts          |   85.86 |    83.87 |      80 |   85.86 | ...47-148,151-153 
  eventBus.ts      |   87.07 |    84.21 |      85 |   87.07 | ...46-354,415-417 
  httpAcpBridge.ts |   78.04 |    77.36 |   95.12 |   78.04 | ...2392,2423-2464 
  index.ts         |       0 |        0 |       0 |       0 | 1-32              
  loopbackBinds.ts |     100 |      100 |     100 |     100 |                   
  runQwenServe.ts  |    76.4 |    89.36 |   83.33 |    76.4 | ...28-344,369-371 
  server.ts        |   82.52 |    79.69 |   82.35 |   82.52 | ...53-758,845-854 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/services      |   92.84 |     90.9 |   98.36 |   92.84 |                   
  ...mandLoader.ts |     100 |     92.3 |     100 |     100 | 91                
  ...killLoader.ts |     100 |    96.15 |     100 |     100 | 45                
  ...andService.ts |   98.75 |      100 |     100 |   98.75 | 111               
  ...ionService.ts |   97.19 |    89.77 |     100 |   97.19 | ...85,423-424,428 
  ...mandLoader.ts |   86.83 |    83.87 |     100 |   86.83 | ...30-335,340-345 
  ...omptLoader.ts |   76.05 |    80.64 |   83.33 |   76.05 | ...12-213,279-280 
  ...mandLoader.ts |     100 |      100 |     100 |     100 |                   
  ...nd-factory.ts |    91.5 |    91.66 |     100 |    91.5 | 129,138-145       
  ...ation-tool.ts |     100 |    95.45 |     100 |     100 | 125               
  ...ndMetadata.ts |   98.21 |    96.66 |     100 |   98.21 | 83,87             
  commandUtils.ts  |      96 |    91.66 |     100 |      96 | 48                
  ...and-parser.ts |   90.69 |    85.71 |     100 |   90.69 | 63-66             
  ...ionService.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...ght/generators |    85.9 |    85.61 |   90.47 |    85.9 |                   
  DataProcessor.ts |   85.63 |     85.6 |   92.85 |   85.63 | ...1122,1126-1133 
  ...tGenerator.ts |   98.21 |    85.71 |     100 |   98.21 | 46                
  ...teRenderer.ts |   45.45 |      100 |       0 |   45.45 | 13-51             
 .../insight/types |       0 |       50 |      50 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 | 1                 
 ...mpt-processors |   97.27 |    94.04 |     100 |   97.27 |                   
  ...tProcessor.ts |     100 |      100 |     100 |     100 |                   
  ...eProcessor.ts |   94.52 |    84.21 |     100 |   94.52 | 46-47,93-94       
  ...tionParser.ts |     100 |      100 |     100 |     100 |                   
  ...lProcessor.ts |   97.41 |    95.65 |     100 |   97.41 | 95-98             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/services/tips |   97.35 |    83.07 |     100 |   97.35 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  tipHistory.ts    |   92.45 |       70 |     100 |   92.45 | ...22,144,151,160 
  tipRegistry.ts   |     100 |    95.23 |     100 |     100 | 33                
  tipScheduler.ts  |     100 |    91.66 |     100 |     100 | 55                
 src/test-utils    |   93.75 |    83.33 |      80 |   93.75 |                   
  ...omMatchers.ts |   69.69 |       50 |      50 |   69.69 | 32-35,37-39,45-47 
  ...andContext.ts |     100 |      100 |     100 |     100 |                   
  render.tsx       |     100 |      100 |     100 |     100 |                   
 src/ui            |   64.47 |    69.23 |   48.93 |   64.47 |                   
  App.tsx          |     100 |      100 |     100 |     100 |                   
  AppContainer.tsx |   66.99 |     64.6 |   52.94 |   66.99 | ...2770,2774-2778 
  ...tionNudge.tsx |    9.58 |      100 |       0 |    9.58 | 24-94             
  ...ackDialog.tsx |   29.23 |      100 |       0 |   29.23 | 25-75             
  ...tionNudge.tsx |    7.69 |      100 |       0 |    7.69 | 25-103            
  colors.ts        |   52.72 |      100 |   23.52 |   52.72 | ...52,54-55,60-61 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  keyMatchers.ts   |   95.91 |    96.42 |     100 |   95.91 | 25-26             
  ...tic-colors.ts |     100 |      100 |     100 |     100 |                   
  textConstants.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/auth       |   48.01 |    58.73 |   21.42 |   48.01 |                   
  AuthDialog.tsx   |   64.26 |    44.44 |   16.66 |   64.26 | ...59,366-388,392 
  ...nProgress.tsx |       0 |        0 |       0 |       0 | 1-64              
  ...etupSteps.tsx |    9.61 |      100 |       0 |    9.61 | ...35-352,391-476 
  useAuth.ts       |   76.63 |    68.29 |     100 |   76.63 | ...48,493-499,560 
  ...rSetupFlow.ts |   44.61 |    33.33 |      50 |   44.61 | ...57-378,395-438 
 src/ui/commands   |   70.19 |    79.57 |   80.45 |   70.19 |                   
  aboutCommand.ts  |     100 |    85.71 |     100 |     100 | 36                
  agentsCommand.ts |   83.78 |      100 |      60 |   83.78 | 30-32,42-44       
  ...odeCommand.ts |     100 |      100 |     100 |     100 |                   
  arenaCommand.ts  |   62.81 |    58.73 |   65.21 |   62.81 | ...91-596,681-689 
  authCommand.ts   |     100 |      100 |     100 |     100 |                   
  branchCommand.ts |     100 |      100 |     100 |     100 |                   
  btwCommand.ts    |   95.59 |    71.42 |     100 |   95.59 | 72,154-159        
  bugCommand.ts    |   81.13 |    71.42 |     100 |   81.13 | 60-69             
  clearCommand.ts  |   92.94 |       75 |     100 |   92.94 | 45-46,74-75,93-94 
  ...essCommand.ts |    64.7 |       50 |      75 |    64.7 | ...48-149,163-166 
  ...extCommand.ts |   34.78 |    22.22 |   45.45 |   34.78 | ...86-521,532-533 
  copyCommand.ts   |   98.28 |    94.89 |     100 |   98.28 | ...80,280,321,327 
  deleteCommand.ts |     100 |      100 |     100 |     100 |                   
  diffCommand.ts   |   99.02 |    86.11 |     100 |   99.02 | 222,226           
  ...ryCommand.tsx |   68.09 |    77.77 |   77.77 |   68.09 | ...56-261,315-323 
  docsCommand.ts   |     100 |    88.88 |     100 |     100 | 25                
  doctorCommand.ts |     100 |    93.33 |     100 |     100 | 21                
  dreamCommand.ts  |      75 |    66.66 |   66.66 |      75 | 22-27,44-47       
  editorCommand.ts |     100 |      100 |     100 |     100 |                   
  exportCommand.ts |      60 |    92.85 |   77.77 |      60 | 176-317           
  ...onsCommand.ts |   48.66 |     90.9 |   63.63 |   48.66 | ...05-109,159-211 
  forgetCommand.ts |   26.82 |      100 |      50 |   26.82 | 18-51             
  helpCommand.ts   |     100 |      100 |     100 |     100 |                   
  hooksCommand.ts  |    20.4 |       40 |      40 |    20.4 | ...48-180,204-205 
  ideCommand.ts    |   60.75 |    64.28 |   41.17 |   60.75 | ...05-306,310-324 
  initCommand.ts   |   84.33 |    72.72 |     100 |   84.33 | 68,82-87,89-94    
  ...ghtCommand.ts |   74.56 |    68.42 |     100 |   74.56 | ...31-245,250-273 
  ...ageCommand.ts |   85.76 |    82.82 |     100 |   85.76 | ...51-658,687-694 
  ...elsCommand.ts |     100 |      100 |     100 |     100 |                   
  mcpCommand.ts    |     100 |      100 |     100 |     100 |                   
  memoryCommand.ts |     100 |      100 |     100 |     100 |                   
  modelCommand.ts  |   74.56 |    79.06 |   71.42 |   74.56 | ...91-200,223-228 
  ...onsCommand.ts |     100 |      100 |     100 |     100 |                   
  planCommand.ts   |   78.82 |    76.92 |     100 |   78.82 | 30-35,51-56,68-73 
  quitCommand.ts   |     100 |      100 |     100 |     100 |                   
  recapCommand.ts  |   21.81 |      100 |      50 |   21.81 | 24-73             
  ...berCommand.ts |   32.43 |      100 |      50 |   32.43 | 23-57             
  renameCommand.ts |   85.29 |    77.77 |     100 |   85.29 | ...06-313,320-325 
  ...oreCommand.ts |    92.3 |    87.87 |     100 |    92.3 | ...,83-88,129-130 
  resumeCommand.ts |     100 |      100 |     100 |     100 |                   
  rewindCommand.ts |      80 |      100 |      50 |      80 | 19-21             
  ...ngsCommand.ts |     100 |      100 |     100 |     100 |                   
  ...hubCommand.ts |   81.43 |    65.21 |      80 |   81.43 | ...70-173,176-179 
  skillsCommand.ts |   15.04 |      100 |      25 |   15.04 | ...90-106,109-136 
  statsCommand.ts  |   88.19 |    84.21 |     100 |   88.19 | ...,58-61,143-146 
  ...ineCommand.ts |     100 |      100 |     100 |     100 |                   
  ...aryCommand.ts |    6.46 |      100 |      50 |    6.46 | 31-329            
  tasksCommand.ts  |   77.45 |    73.43 |     100 |   77.45 | ...55-159,181-186 
  ...tupCommand.ts |     100 |      100 |     100 |     100 |                   
  themeCommand.ts  |     100 |      100 |     100 |     100 |                   
  toolsCommand.ts  |     100 |      100 |     100 |     100 |                   
  trustCommand.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
  vimCommand.ts    |   54.54 |      100 |      50 |   54.54 | 19-29             
 src/ui/components |   61.19 |    75.11 |   66.66 |   61.19 |                   
  AboutBox.tsx     |     100 |      100 |     100 |     100 |                   
  AnsiOutput.tsx   |   65.57 |      100 |      50 |   65.57 | 69-90             
  ApiKeyInput.tsx  |       0 |        0 |       0 |       0 | 1-97              
  AppHeader.tsx    |   89.39 |       75 |     100 |   89.39 | 35,37-42,44       
  ...odeDialog.tsx |     9.7 |      100 |       0 |     9.7 | 35-47,50-182      
  AsciiArt.ts      |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |   14.63 |      100 |       0 |   14.63 | 18-56             
  ...TextInput.tsx |   77.01 |       76 |     100 |   77.01 | ...20,234-236,263 
  Composer.tsx     |    80.8 |     64.7 |     100 |    80.8 | ...85,103,154,167 
  ...entPrompt.tsx |     100 |      100 |     100 |     100 |                   
  ...ryDisplay.tsx |   75.89 |    62.06 |     100 |   75.89 | ...,88,93-108,113 
  ...geDisplay.tsx |   68.42 |    57.14 |     100 |   68.42 | 16-17,31-32,42-50 
  ...ification.tsx |   28.57 |      100 |       0 |   28.57 | 16-36             
  ...gProfiler.tsx |       0 |        0 |       0 |       0 | 1-36              
  ...ogManager.tsx |    12.4 |      100 |       0 |    12.4 | 63-474            
  ...ngsDialog.tsx |    8.44 |      100 |       0 |    8.44 | 37-195            
  ExitWarning.tsx  |     100 |      100 |     100 |     100 |                   
  ...hProgress.tsx |    87.8 |    33.33 |     100 |    87.8 | 28-31,56          
  ...ustDialog.tsx |     100 |      100 |     100 |     100 |                   
  Footer.tsx       |   79.67 |    58.06 |     100 |   79.67 | ...98-102,104-108 
  ...ngSpinner.tsx |   68.42 |       80 |      50 |   68.42 | 35-52,73,80-81    
  Header.tsx       |   98.62 |    94.28 |     100 |   98.62 | 162,164           
  Help.tsx         |   98.32 |    89.88 |     100 |   98.32 | ...24,381,447-448 
  ...emDisplay.tsx |   63.27 |    36.73 |     100 |   63.27 | ...29-338,341,344 
  ...ngeDialog.tsx |     100 |      100 |     100 |     100 |                   
  InputPrompt.tsx  |   82.25 |    77.43 |   83.33 |   82.25 | ...1347,1412,1462 
  ...Shortcuts.tsx |   20.87 |      100 |       0 |   20.87 | ...6,49-51,67-125 
  ...Indicator.tsx |     100 |    91.42 |     100 |     100 | 65,74             
  ...firmation.tsx |   91.42 |      100 |      50 |   91.42 | 26-31             
  MainContent.tsx  |   81.75 |       75 |     100 |   81.75 | ...70-274,282-286 
  ...elsDialog.tsx |   16.07 |    89.18 |      50 |   16.07 | ...58-159,162-648 
  MemoryDialog.tsx |   53.21 |    51.21 |   57.14 |   53.21 | ...54,366,379-381 
  ...geDisplay.tsx |       0 |        0 |       0 |       0 | 1-41              
  ModelDialog.tsx  |   76.31 |    54.94 |     100 |   76.31 | ...05-521,578-582 
  ...tsDisplay.tsx |     100 |    97.22 |     100 |     100 | 270               
  ...fications.tsx |   18.18 |      100 |       0 |   18.18 | 15-58             
  ...onsDialog.tsx |    2.13 |      100 |       0 |    2.13 | 62-133,148-1004   
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...icePrompt.tsx |   88.14 |    83.87 |     100 |   88.14 | ...01-105,133-138 
  PrepareLabel.tsx |   91.66 |    77.27 |     100 |   91.66 | 73-75,77-79,110   
  ...atePrompt.tsx |    8.57 |      100 |       0 |    8.57 | 24-55,58-134      
  ...geDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...ngDisplay.tsx |   21.42 |      100 |       0 |   21.42 | 13-39             
  ...hProgress.tsx |   85.25 |    88.46 |     100 |   85.25 | 121-147           
  ...dSelector.tsx |    4.45 |      100 |       0 |    4.45 | 28-92,100-328     
  ...ionPicker.tsx |   78.43 |    66.66 |     100 |   78.43 | ...20-422,444-466 
  ...onPreview.tsx |   92.42 |    84.37 |     100 |   92.42 | ...,70-71,143-145 
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...putPrompt.tsx |   72.56 |       80 |      40 |   72.56 | ...06-109,114-117 
  ...ngsDialog.tsx |   66.88 |    73.52 |     100 |   66.88 | ...11-819,825-826 
  ...ionDialog.tsx |    87.8 |      100 |   33.33 |    87.8 | 36-39,44-51       
  ...putPrompt.tsx |    15.9 |      100 |       0 |    15.9 | 20-63             
  ...Indicator.tsx |   57.14 |      100 |       0 |   57.14 | 12-15             
  ...MoreLines.tsx |      28 |      100 |       0 |      28 | 18-40             
  ...ionPicker.tsx |   17.59 |      100 |       0 |   17.59 | 55-172            
  StatsDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...yTodoList.tsx |   94.17 |       80 |     100 |   94.17 | 56-57,131-134     
  ...nsDisplay.tsx |   87.25 |       64 |     100 |   87.25 | ...45-147,154-156 
  ThemeDialog.tsx  |   89.95 |    46.15 |      75 |   89.95 | ...71-173,243-245 
  Tips.tsx         |   93.54 |       75 |     100 |   93.54 | 39-40             
  TodoDisplay.tsx  |     100 |      100 |     100 |     100 |                   
  ...tsDisplay.tsx |     100 |     87.5 |     100 |     100 | 31-32             
  TrustDialog.tsx  |     100 |    81.81 |     100 |     100 | 71-86             
  ...ification.tsx |   36.36 |      100 |       0 |   36.36 | 15-22             
  ...ackDialog.tsx |    7.84 |      100 |       0 |    7.84 | 24-134            
 ...nts/agent-view |    25.2 |       90 |      10 |    25.2 |                   
  ...atContent.tsx |    8.79 |      100 |       0 |    8.79 | 53-265,271-273    
  ...tChatView.tsx |   21.05 |      100 |       0 |   21.05 | 21-39             
  ...tComposer.tsx |    9.95 |      100 |       0 |    9.95 | 57-308            
  AgentFooter.tsx  |   17.07 |      100 |       0 |   17.07 | 28-66             
  AgentHeader.tsx  |   15.38 |      100 |       0 |   15.38 | 27-64             
  AgentTabBar.tsx  |    8.13 |      100 |       0 |    8.13 | 39-59,64-187      
  ...oryAdapter.ts |     100 |    91.83 |     100 |     100 | 103,109-110,138   
  index.ts         |       0 |        0 |       0 |       0 | 1-12              
 ...mponents/arena |   45.72 |    70.53 |   60.86 |   45.72 |                   
  ArenaCards.tsx   |   73.06 |    71.79 |   85.71 |   73.06 | ...83-185,321-326 
  ...ectDialog.tsx |   83.48 |    69.86 |   88.88 |   83.48 | ...88-392,409-410 
  ...artDialog.tsx |   10.15 |      100 |       0 |   10.15 | 27-161            
  ...tusDialog.tsx |    5.63 |      100 |       0 |    5.63 | 33-75,80-288      
  ...topDialog.tsx |    6.17 |      100 |       0 |    6.17 | 33-213            
 ...ackground-view |   75.44 |     83.6 |   85.29 |   75.44 |                   
  ...sksDialog.tsx |   70.05 |       79 |   76.19 |   70.05 | ...1119,1195-1197 
  ...TasksPill.tsx |   70.83 |    86.95 |     100 |   70.83 | 44,84-96,104-112  
  ...gentPanel.tsx |   99.52 |    93.18 |     100 |   99.52 | 123               
 ...nts/extensions |   45.28 |    33.33 |      60 |   45.28 |                   
  ...gerDialog.tsx |   44.31 |    34.14 |      75 |   44.31 | ...71-480,483-488 
  index.ts         |       0 |        0 |       0 |       0 | 1-9               
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...tensions/steps |   54.77 |    94.23 |   66.66 |   54.77 |                   
  ...ctionStep.tsx |   95.12 |    92.85 |   85.71 |   95.12 | 84-86,89          
  ...etailStep.tsx |    6.18 |      100 |       0 |    6.18 | 17-128            
  ...nListStep.tsx |   88.35 |    94.73 |      80 |   88.35 | 51-52,58-71,105   
  ...electStep.tsx |   13.46 |      100 |       0 |   13.46 | 20-70             
  ...nfirmStep.tsx |   19.56 |      100 |       0 |   19.56 | 23-65             
  index.ts         |     100 |      100 |     100 |     100 |                   
 ...mponents/hooks |   72.24 |    70.52 |      80 |   72.24 |                   
  ...etailStep.tsx |   96.52 |       75 |     100 |   96.52 | 33,37,50,59       
  ...etailStep.tsx |   93.27 |    73.68 |     100 |   93.27 | 41-42,99-104,110  
  ...abledStep.tsx |     100 |      100 |     100 |     100 |                   
  ...sListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...entDialog.tsx |   36.09 |    47.05 |      50 |   36.09 | ...49,453-466,470 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-13              
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...components/mcp |   20.83 |    83.72 |   83.33 |   20.83 |                   
  ...ealthPill.tsx |   68.42 |    85.71 |     100 |   68.42 | 40-46             
  ...entDialog.tsx |    3.64 |      100 |       0 |    3.64 | 41-717            
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-30              
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |   94.79 |    85.71 |     100 |   94.79 | 16,20,35,109-110  
 ...ents/mcp/steps |    6.88 |      100 |       0 |    6.88 |                   
  ...icateStep.tsx |    5.88 |      100 |       0 |    5.88 | 40-55,58-296      
  ...electStep.tsx |   10.95 |      100 |       0 |   10.95 | 16-88             
  ...etailStep.tsx |    5.26 |      100 |       0 |    5.26 | 31-247            
  ...rListStep.tsx |    5.88 |      100 |       0 |    5.88 | 20-176            
  ...etailStep.tsx |   10.41 |      100 |       0 |   10.41 | ...1,67-79,82-139 
  ToolListStep.tsx |    7.14 |      100 |       0 |    7.14 | 16-146            
 ...nents/messages |   82.15 |    80.23 |   72.85 |   82.15 |                   
  ...ionDialog.tsx |   77.35 |    74.54 |    62.5 |   77.35 | ...90,508,526-528 
  BtwMessage.tsx   |     100 |      100 |     100 |     100 |                   
  ...upDisplay.tsx |   97.67 |    83.72 |     100 |   97.67 | 119,142,150       
  ...onMessage.tsx |   91.93 |    82.35 |     100 |   91.93 | 57-59,61,63       
  ...nMessages.tsx |   79.06 |      100 |      70 |   79.06 | ...51-264,268-280 
  DiffRenderer.tsx |   93.19 |    86.17 |     100 |   93.19 | ...09,237-238,304 
  ...tsDisplay.tsx |   97.82 |    77.27 |     100 |   97.82 | 87,89             
  ...ssMessage.tsx |    12.5 |      100 |       0 |    12.5 | 18-59             
  ...edMessage.tsx |   16.66 |      100 |       0 |   16.66 | 22-38             
  ...sMessages.tsx |   55.67 |       40 |   28.57 |   55.67 | ...20-125,133-145 
  ...ryMessage.tsx |   14.28 |      100 |       0 |   14.28 | 23-62             
  ...onMessage.tsx |   81.02 |    69.23 |   33.33 |   81.02 | ...24-426,433-435 
  ...upMessage.tsx |      84 |    93.61 |     100 |      84 | ...56-383,405-420 
  ToolMessage.tsx  |   88.84 |    75.71 |    92.3 |   88.84 | ...44-749,776-778 
 ...ponents/shared |   82.37 |    77.36 |   92.75 |   82.37 |                   
  ...ctionList.tsx |   99.03 |    95.65 |     100 |   99.03 | 85                
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  EnumSelector.tsx |     100 |    96.42 |     100 |     100 | 58                
  MaxSizedBox.tsx  |   83.01 |    86.25 |   88.88 |   83.01 | ...12-513,618-619 
  MultiSelect.tsx  |    6.29 |      100 |       0 |    6.29 | 35-42,45-176      
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  ...eSelector.tsx |     100 |       60 |     100 |     100 | 40-45             
  TextInput.tsx    |   72.98 |    55.55 |      80 |   72.98 | ...08-212,224-230 
  ...apsedTime.tsx |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |     100 |      100 |     100 |     100 |                   
  text-buffer.ts   |   83.62 |    75.62 |   97.61 |   83.62 | ...2272,2300,2368 
  ...er-actions.ts |   86.71 |    67.79 |     100 |   86.71 | ...07-608,809-811 
 ...ents/subagents |   30.87 |        0 |       0 |   30.87 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-11              
  reducers.tsx     |    12.1 |      100 |       0 |    12.1 | 33-190            
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |   10.95 |      100 |       0 |   10.95 | ...1,56-57,60-102 
 ...bagents/create |    9.13 |      100 |       0 |    9.13 |                   
  ...ionWizard.tsx |    7.28 |      100 |       0 |    7.28 | 34-299            
  ...rSelector.tsx |   14.75 |      100 |       0 |   14.75 | 26-85             
  ...onSummary.tsx |    4.26 |      100 |       0 |    4.26 | 27-331            
  ...tionInput.tsx |    8.63 |      100 |       0 |    8.63 | 23-177            
  ...dSelector.tsx |   33.33 |      100 |       0 |   33.33 | 20-21,26-27,36-63 
  ...nSelector.tsx |    37.5 |      100 |       0 |    37.5 | 20-21,26-27,36-58 
  ...EntryStep.tsx |   12.76 |      100 |       0 |   12.76 | 34-78             
  ToolSelector.tsx |    4.16 |      100 |       0 |    4.16 | 31-253            
 ...bagents/manage |    8.39 |      100 |       0 |    8.39 |                   
  ...ctionStep.tsx |   10.25 |      100 |       0 |   10.25 | 21-103            
  ...eleteStep.tsx |   20.93 |      100 |       0 |   20.93 | 23-62             
  ...tEditStep.tsx |   25.53 |      100 |       0 |   25.53 | ...2,37-38,51-124 
  ...ctionStep.tsx |    2.29 |      100 |       0 |    2.29 | 28-449            
  ...iewerStep.tsx |   13.72 |      100 |       0 |   13.72 | 18-73             
  ...gerDialog.tsx |    6.74 |      100 |       0 |    6.74 | 35-341            
 ...mponents/views |   42.16 |    69.23 |   21.42 |   42.16 |                   
  ContextUsage.tsx |     4.7 |      100 |       0 |     4.7 | ...52-167,170-456 
  DoctorReport.tsx |     9.8 |      100 |       0 |     9.8 | 25-54,57-131      
  ...sionsList.tsx |   87.69 |    73.68 |     100 |   87.69 | 65-72             
  McpStatus.tsx    |   89.53 |    60.52 |     100 |   89.53 | ...72,175-177,262 
  SkillsList.tsx   |   27.27 |      100 |       0 |   27.27 | 18-35             
  ToolsList.tsx    |     100 |      100 |     100 |     100 |                   
 src/ui/contexts   |   77.05 |    78.24 |   82.14 |   77.05 |                   
  ...ewContext.tsx |   65.77 |      100 |      75 |   65.77 | ...22-225,231-241 
  AppContext.tsx   |      80 |       50 |     100 |      80 | 19-20             
  ...ewContext.tsx |   93.37 |    68.57 |      50 |   93.37 | ...94-195,222-226 
  ...deContext.tsx |     100 |      100 |     100 |     100 |                   
  ...igContext.tsx |   81.81 |       50 |     100 |   81.81 | 15-16             
  ...ssContext.tsx |   81.88 |    82.26 |     100 |   81.88 | ...1153,1159-1161 
  ...owContext.tsx |   89.28 |       80 |   66.66 |   89.28 | 34,47-48,60-62    
  ...deContext.tsx |     100 |      100 |      50 |     100 |                   
  ...onContext.tsx |   43.28 |     62.5 |    62.5 |   43.28 | ...56-259,263-266 
  ...gsContext.tsx |   83.33 |       50 |     100 |   83.33 | 17-18             
  ...usContext.tsx |     100 |      100 |     100 |     100 |                   
  ...ngContext.tsx |   71.42 |       50 |     100 |   71.42 | 17-20             
  ...utContext.tsx |   85.71 |      100 |   66.66 |   85.71 | 13-14             
  ...nsContext.tsx |   88.23 |       50 |     100 |   88.23 | 109-110           
  ...teContext.tsx |   86.66 |       50 |     100 |   86.66 | 173-174           
  ...deContext.tsx |   76.08 |    72.72 |     100 |   76.08 | 47-48,52-59,77-78 
 src/ui/editors    |   93.33 |    85.71 |   66.66 |   93.33 |                   
  ...ngsManager.ts |   93.33 |    85.71 |   66.66 |   93.33 | 49,63-64          
 src/ui/hooks      |    81.9 |    81.98 |   86.47 |    81.9 |                   
  ...dProcessor.ts |   83.12 |    82.56 |     100 |   83.12 | ...88-389,408-435 
  keyToAnsi.ts     |    3.92 |      100 |       0 |    3.92 | 19-77             
  ...dProcessor.ts |    94.8 |    70.58 |     100 |    94.8 | ...76-277,282-283 
  ...dProcessor.ts |    75.9 |    63.44 |   61.53 |    75.9 | ...84,908,927-931 
  ...amingState.ts |   12.22 |      100 |       0 |   12.22 | 54-158            
  ...agerDialog.ts |   88.23 |      100 |     100 |   88.23 | 20,24             
  ...ationFrame.ts |      32 |       60 |     100 |      32 | 42-44,51-90       
  ...odeCommand.ts |   58.82 |      100 |     100 |   58.82 | 28,33-48          
  ...enaCommand.ts |      85 |      100 |     100 |      85 | 23-24,29          
  ...aInProcess.ts |   19.81 |    66.66 |      25 |   19.81 | 57-175            
  ...Completion.ts |   92.77 |    89.09 |     100 |   92.77 | ...86-187,220-223 
  ...ifications.ts |   92.07 |    96.29 |     100 |   92.07 | 116-124           
  ...tIndicator.ts |     100 |    93.75 |     100 |     100 | 63                
  ...waySummary.ts |   96.22 |    69.69 |     100 |   96.22 | 125-127,169       
  ...ndTaskView.ts |   94.11 |    76.92 |     100 |   94.11 | 119-123,216,222   
  ...ketedPaste.ts |    23.8 |      100 |       0 |    23.8 | 19-37             
  ...nchCommand.ts |   93.75 |    73.17 |     100 |   93.75 | ...68-169,221-222 
  ...ompletion.tsx |   95.95 |    82.75 |     100 |   95.95 | ...22-223,225-226 
  ...dMigration.ts |   90.62 |       75 |     100 |   90.62 | 38-40             
  useCompletion.ts |    92.4 |     87.5 |     100 |    92.4 | 68-69,93-94,98-99 
  ...nitMessage.ts |     100 |      100 |     100 |     100 |                   
  ...extualTips.ts |   76.92 |       50 |     100 |   76.92 | 55,68,71-75,88-96 
  ...eteCommand.ts |   78.53 |    88.57 |     100 |   78.53 | ...96-104,112-113 
  ...ialogClose.ts |   16.66 |      100 |     100 |   16.66 | 79-139            
  ...oublePress.ts |   53.12 |       75 |     100 |   53.12 | 33-35,41-54       
  ...orSettings.ts |     100 |      100 |     100 |     100 |                   
  ...Completion.ts |   99.12 |     97.7 |     100 |   99.12 | 182-183           
  ...ionUpdates.ts |   93.45 |     92.3 |     100 |   93.45 | ...83-287,300-306 
  ...agerDialog.ts |   88.88 |      100 |     100 |   88.88 | 21,25             
  ...backDialog.ts |   54.47 |       50 |   33.33 |   54.47 | ...69-171,193-194 
  useFocus.ts      |     100 |      100 |     100 |     100 |                   
  ...olderTrust.ts |     100 |      100 |     100 |     100 |                   
  ...ggestions.tsx |   89.15 |     62.5 |      50 |   89.15 | ...22-124,149-150 
  ...miniStream.ts |   76.64 |    73.93 |   91.66 |   76.64 | ...2425,2438-2446 
  ...BranchName.ts |    90.9 |     92.3 |     100 |    90.9 | 19-20,55-58       
  ...oryManager.ts |   93.15 |    93.75 |     100 |   93.15 | 44,107-110        
  ...ooksDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...stListener.ts |     100 |      100 |     100 |     100 |                   
  ...nAuthError.ts |   76.19 |       50 |     100 |   76.19 | 39-40,43-45       
  ...putHistory.ts |   92.59 |    85.71 |     100 |   92.59 | 63-64,72,94-96    
  ...storyStore.ts |     100 |    94.11 |     100 |     100 | 69                
  useKeypress.ts   |     100 |      100 |     100 |     100 |                   
  ...rdProtocol.ts |   36.36 |      100 |       0 |   36.36 | 24-31             
  ...unchEditor.ts |    9.67 |      100 |       0 |    9.67 | 11-32,39-90       
  ...gIndicator.ts |     100 |      100 |     100 |     100 |                   
  useLogger.ts     |   21.05 |      100 |       0 |   21.05 | 15-37             
  useMCPHealth.ts  |   63.15 |       75 |      50 |   63.15 | 42-52,64-67       
  ...elsCommand.ts |     100 |      100 |     100 |     100 |                   
  useMcpDialog.ts  |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...moryDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...oryMonitor.ts |     100 |      100 |     100 |     100 |                   
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...delCommand.ts |     100 |       75 |     100 |     100 | 22                
  ...raseCycler.ts |   84.74 |    76.47 |     100 |   84.74 | ...49,52-53,69-71 
  ...derUpdates.ts |   86.38 |    77.19 |     100 |   86.38 | ...22,281-293,341 
  useQwenAuth.ts   |     100 |      100 |     100 |     100 |                   
  ...lScheduler.ts |    84.7 |    93.33 |     100 |    84.7 | ...71-276,372-382 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-7               
  ...umeCommand.ts |   97.24 |    76.92 |     100 |   97.24 | 104-105,145       
  ...ompletion.tsx |   90.59 |    83.33 |     100 |   90.59 | ...01,104,137-140 
  ...ectionList.ts |   96.96 |    95.69 |     100 |   96.96 | ...82-183,237-240 
  ...sionPicker.ts |   85.67 |    81.37 |     100 |   85.67 | ...25-527,536-538 
  ...earchInput.ts |     100 |      100 |     100 |     100 |                   
  ...ngsCommand.ts |   18.75 |      100 |       0 |   18.75 | 10-25             
  ...ellHistory.ts |   91.74 |    79.41 |     100 |   91.74 | ...74,122-123,133 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-73              
  ...Completion.ts |   82.67 |    85.41 |   94.73 |   82.67 | ...68-670,678-714 
  ...tateAndRef.ts |     100 |      100 |     100 |     100 |                   
  useStatusLine.ts |     100 |    98.79 |     100 |     100 | 257               
  ...eateDialog.ts |   88.23 |      100 |     100 |   88.23 | 14,18             
  ...tification.ts |     100 |    85.71 |     100 |     100 | 47                
  ...alProgress.ts |   53.06 |       50 |   66.66 |   53.06 | ...53,61-68,79-85 
  ...rminalSize.ts |   76.19 |      100 |      50 |   76.19 | 21-25             
  ...emeCommand.ts |   67.01 |    29.41 |     100 |   67.01 | ...10-111,115-116 
  useTimer.ts      |   88.09 |    85.71 |     100 |   88.09 | 44-45,51-53       
  ...lMigration.ts |       0 |        0 |       0 |       0 |                   
  ...rustModify.ts |     100 |      100 |     100 |     100 |                   
  ...elcomeBack.ts |   87.36 |     90.9 |     100 |   87.36 | ...,94-96,114-115 
  vim.ts           |   83.77 |    80.31 |     100 |   83.77 | ...55,759-767,776 
 src/ui/layouts    |   89.72 |     87.5 |     100 |   89.72 |                   
  ...AppLayout.tsx |   89.88 |     87.5 |     100 |   89.88 | 51-53,93-98       
  ...AppLayout.tsx |   89.47 |     87.5 |     100 |   89.47 | 58-63             
 ...i/manageModels |   93.61 |       48 |     100 |   93.61 |                   
  manageModels.ts  |   93.61 |       48 |     100 |   93.61 | ...63-166,179,209 
 src/ui/models     |   80.24 |    79.16 |   71.42 |   80.24 |                   
  ...ableModels.ts |   80.24 |    79.16 |   71.42 |   80.24 | ...,61-71,123-125 
 ...noninteractive |     100 |      100 |    7.14 |     100 |                   
  ...eractiveUi.ts |     100 |      100 |    7.14 |     100 |                   
 src/ui/state      |   94.91 |    81.81 |     100 |   94.91 |                   
  extensions.ts    |   94.91 |    81.81 |     100 |   94.91 | 68-69,88          
 src/ui/themes     |   98.53 |    70.58 |     100 |   98.53 |                   
  ansi-light.ts    |     100 |      100 |     100 |     100 |                   
  ansi.ts          |     100 |      100 |     100 |     100 |                   
  atom-one-dark.ts |     100 |      100 |     100 |     100 |                   
  ayu-light.ts     |     100 |      100 |     100 |     100 |                   
  ayu.ts           |     100 |      100 |     100 |     100 |                   
  color-utils.ts   |     100 |      100 |     100 |     100 |                   
  default-light.ts |     100 |      100 |     100 |     100 |                   
  default.ts       |     100 |      100 |     100 |     100 |                   
  ...inal-theme.ts |   88.59 |    85.96 |     100 |   88.59 | ...57-261,266-270 
  dracula.ts       |     100 |      100 |     100 |     100 |                   
  github-dark.ts   |     100 |      100 |     100 |     100 |                   
  github-light.ts  |     100 |      100 |     100 |     100 |                   
  googlecode.ts    |     100 |      100 |     100 |     100 |                   
  no-color.ts      |     100 |      100 |     100 |     100 |                   
  qwen-dark.ts     |     100 |      100 |     100 |     100 |                   
  qwen-light.ts    |     100 |      100 |     100 |     100 |                   
  ...tic-tokens.ts |     100 |      100 |     100 |     100 |                   
  ...-of-purple.ts |     100 |      100 |     100 |     100 |                   
  theme-manager.ts |   87.98 |    82.89 |     100 |   87.98 | ...48-357,362-363 
  theme.ts         |     100 |    38.02 |     100 |     100 | ...34-449,457-461 
  xcode.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/utils      |   83.69 |    82.69 |    92.3 |   83.69 |                   
  ...Colorizer.tsx |   82.78 |    88.23 |     100 |   82.78 | ...10-111,197-223 
  ...nRenderer.tsx |   68.83 |    70.14 |      50 |   68.83 | ...52-254,274-293 
  ...wnDisplay.tsx |   86.01 |    87.41 |     100 |   86.01 | ...87,704,729-754 
  ...idDiagram.tsx |   87.79 |    95.34 |     100 |   87.79 | 156-179           
  ...eRenderer.tsx |   92.08 |    80.45 |      95 |   92.08 | ...76-679,723-728 
  ...dWorkUtils.ts |     100 |      100 |     100 |     100 |                   
  ...boardUtils.ts |   59.61 |    58.82 |     100 |   59.61 | ...,86-88,107-149 
  commandUtils.ts  |    95.9 |    88.29 |     100 |    95.9 | ...62,164-165,289 
  computeStats.ts  |     100 |      100 |     100 |     100 |                   
  customBanner.ts  |   90.68 |    91.22 |     100 |   90.68 | ...13,324-327,334 
  displayUtils.ts  |   88.37 |    72.22 |     100 |   88.37 | 23,25,29,31,33    
  formatters.ts    |   95.23 |    98.27 |     100 |   95.23 | 117-120           
  gradientUtils.ts |     100 |      100 |     100 |     100 |                   
  highlight.ts     |     100 |      100 |     100 |     100 |                   
  ...oryMapping.ts |     100 |    94.28 |     100 |     100 | 29,51             
  historyUtils.ts  |   94.02 |    93.87 |     100 |   94.02 | 93-96             
  isNarrowWidth.ts |     100 |      100 |     100 |     100 |                   
  ...olDetector.ts |    8.23 |      100 |       0 |    8.23 | ...31-132,135-136 
  latexRenderer.ts |   94.95 |     73.8 |     100 |   94.95 | ...76-178,184-187 
  layoutUtils.ts   |     100 |      100 |     100 |     100 |                   
  ...nUtilities.ts |   69.84 |    85.71 |     100 |   69.84 | 75-91,100-101     
  ...ToolGroups.ts |   98.66 |    96.77 |     100 |   98.66 | 48-49             
  ...geRenderer.ts |   86.23 |    69.06 |   95.12 |   86.23 | ...1284,1324-1330 
  ...alRenderer.ts |   86.69 |     71.9 |     100 |   86.69 | ...1476,1513-1519 
  ...lsBySource.ts |     100 |    95.23 |     100 |     100 | 84                
  osc8.ts          |   94.71 |    87.41 |     100 |   94.71 | ...43,428,432-433 
  ...mConstants.ts |     100 |      100 |     100 |     100 |                   
  ...storyUtils.ts |   61.06 |    69.62 |      90 |   61.06 | ...64,412,417-439 
  ...ickerUtils.ts |     100 |      100 |     100 |     100 |                   
  ...izedOutput.ts |   94.94 |      100 |   88.88 |   94.94 | 112-117           
  ...wOptimizer.ts |     100 |    96.77 |     100 |     100 | 69                
  terminalSetup.ts |    4.37 |      100 |       0 |    4.37 | 44-393            
  textUtils.ts     |   97.35 |    94.38 |   91.66 |   97.35 | ...50-251,386-387 
  todoSnapshot.ts  |   89.11 |    93.33 |     100 |   89.11 | ...,66-78,180-181 
  updateCheck.ts   |     100 |    80.95 |     100 |     100 | 30-42             
 ...i/utils/export |   56.77 |     40.8 |   79.41 |   56.77 |                   
  collect.ts       |   55.92 |    50.58 |   86.36 |   55.92 | ...25-640,642-647 
  index.ts         |     100 |      100 |     100 |     100 |                   
  normalize.ts     |   57.47 |    20.51 |      80 |   57.47 | ...09-310,324-359 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
  utils.ts         |      40 |      100 |       0 |      40 | 11-13             
 ...ort/formatters |    3.38 |      100 |       0 |    3.38 |                   
  html.ts          |    9.61 |      100 |       0 |    9.61 | ...28,34-76,82-84 
  json.ts          |      50 |      100 |       0 |      50 | 14-15             
  jsonl.ts         |     3.5 |      100 |       0 |     3.5 | 14-76             
  markdown.ts      |    0.94 |      100 |       0 |    0.94 | 13-295            
 src/utils         |   73.96 |    90.16 |   93.89 |   73.96 |                   
  acpModelUtils.ts |     100 |      100 |     100 |     100 |                   
  apiPreconnect.ts |   96.52 |    97.05 |     100 |   96.52 | 164-167           
  checks.ts        |   33.33 |      100 |       0 |   33.33 | 23-28             
  cleanup.ts       |   84.12 |    93.33 |      80 |   84.12 | 75,106-115        
  commands.ts      |     100 |      100 |     100 |     100 |                   
  commentJson.ts   |   87.17 |    90.47 |     100 |   87.17 | 64-73             
  ...Calculator.ts |     100 |      100 |     100 |     100 |                   
  deepMerge.ts     |     100 |       90 |     100 |     100 | 41-43,49          
  ...ScopeUtils.ts |   97.56 |    88.88 |     100 |   97.56 | 67                
  doctorChecks.ts  |   71.06 |       75 |     100 |   71.06 | ...95-301,325-341 
  ...putCapture.ts |   90.65 |    86.17 |     100 |   90.65 | ...72,370,372-373 
  ...arResolver.ts |   94.28 |       88 |     100 |   94.28 | 28-29,125-126     
  errors.ts        |   98.67 |    96.36 |     100 |   98.67 | 67-68             
  events.ts        |     100 |      100 |     100 |     100 |                   
  gitUtils.ts      |   91.91 |    84.61 |     100 |   91.91 | 78-81,124-127     
  ...AutoUpdate.ts |   90.76 |    93.33 |   88.88 |   90.76 | 103-114           
  ...lationInfo.ts |     100 |      100 |     100 |     100 |                   
  languageUtils.ts |   97.89 |    96.42 |     100 |   97.89 | 132-133           
  math.ts          |       0 |        0 |       0 |       0 | 1-15              
  ...onfigUtils.ts |     100 |      100 |     100 |     100 |                   
  ...iveHelpers.ts |   96.82 |    93.28 |     100 |   96.82 | ...84-485,583,596 
  osc.ts           |    97.5 |      100 |   88.88 |    97.5 | 195-196           
  package.ts       |   88.88 |       80 |     100 |   88.88 | 33-34             
  processUtils.ts  |     100 |      100 |     100 |     100 |                   
  readStdin.ts     |   79.62 |       90 |      80 |   79.62 | 33-40,52-54       
  relaunch.ts      |   98.07 |    76.92 |     100 |   98.07 | 70                
  resolvePath.ts   |   66.66 |       25 |     100 |   66.66 | 12-13,16,18-19    
  sandbox.ts       |       0 |        0 |       0 |       0 | 1-1047            
  settingsUtils.ts |   82.89 |    90.67 |   89.47 |   82.89 | ...52-663,670-678 
  spawnWrapper.ts  |     100 |      100 |     100 |     100 |                   
  ...upProfiler.ts |   98.46 |    94.52 |     100 |   98.46 | 130-131,305       
  ...upWarnings.ts |     100 |      100 |     100 |     100 |                   
  stdioHelpers.ts  |     100 |       60 |     100 |     100 | 23,32             
  systemInfo.ts    |   92.52 |     90.9 |   83.33 |   92.52 | 63-69,184         
  ...InfoFields.ts |    87.5 |     64.1 |     100 |    87.5 | ...21-122,143-144 
  ...iffPreview.ts |   94.11 |    83.33 |     100 |   94.11 | 13                
  ...entEmitter.ts |     100 |      100 |     100 |     100 |                   
  ...upWarnings.ts |   91.17 |    82.35 |     100 |   91.17 | 67-68,73-74,77-78 
  version.ts       |     100 |       50 |     100 |     100 | 11                
  windowTitle.ts   |     100 |      100 |     100 |     100 |                   
  ...WithBackup.ts |   63.15 |    81.25 |     100 |   63.15 | 93,118-157        
-------------------|---------|----------|---------|---------|-------------------
Core Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |    78.4 |    82.66 |   81.01 |    78.4 |                   
 src               |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/__mocks__/fs  |       0 |        0 |       0 |       0 |                   
  promises.ts      |       0 |        0 |       0 |       0 | 1-48              
 src/agents        |   86.11 |    76.88 |   91.66 |   86.11 |                   
  ...transcript.ts |   88.92 |    76.66 |     100 |   88.92 | ...82,306-307,438 
  ...ent-resume.ts |   81.23 |    69.89 |   77.41 |   81.23 | ...1021,1024-1026 
  ...ound-tasks.ts |   95.13 |    86.61 |     100 |   95.13 | ...06-707,733-734 
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/agents/arena  |   76.98 |    67.72 |   78.72 |   76.98 |                   
  ...gentClient.ts |   79.47 |    88.88 |   81.81 |   79.47 | ...68-183,189-204 
  ArenaManager.ts  |   75.92 |    64.19 |   78.26 |   75.92 | ...1860,1866-1867 
  arena-events.ts  |   64.44 |      100 |      50 |   64.44 | ...71-175,178-183 
  diff-summary.ts  |    87.5 |    73.46 |     100 |    87.5 | ...32-133,137-138 
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...gents/backends |   76.29 |    86.15 |   73.04 |   76.29 |                   
  ITermBackend.ts  |   97.97 |    93.93 |     100 |   97.97 | ...78-180,255,307 
  ...essBackend.ts |   91.25 |    90.62 |   86.66 |   91.25 | ...94,249-269,328 
  TmuxBackend.ts   |    90.7 |    76.55 |   97.36 |    90.7 | ...87,697,743-747 
  detect.ts        |   31.25 |      100 |       0 |   31.25 | 34-88             
  index.ts         |     100 |      100 |     100 |     100 |                   
  iterm-it2.ts     |     100 |     92.1 |     100 |     100 | 37-38,106         
  tmux-commands.ts |    6.64 |      100 |    3.03 |    6.64 | ...93-363,386-503 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...agents/runtime |   81.13 |     76.7 |   71.42 |   81.13 |                   
  agent-context.ts |     100 |      100 |     100 |     100 |                   
  agent-core.ts    |   76.45 |    72.35 |   60.86 |   76.45 | ...1604,1631-1677 
  agent-events.ts  |     100 |      100 |     100 |     100 |                   
  ...t-headless.ts |   81.19 |    71.73 |   60.86 |   81.19 | ...98-399,402-403 
  ...nteractive.ts |   79.71 |    79.62 |      75 |   79.71 | ...54,456,458,461 
  ...statistics.ts |   98.19 |    82.35 |     100 |   98.19 | 127,151,192,225   
  agent-types.ts   |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/config        |    76.6 |    78.42 |   62.55 |    76.6 |                   
  config.ts        |   74.54 |    76.24 |   57.56 |   74.54 | ...3258,3269-3281 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  models.ts        |     100 |      100 |     100 |     100 |                   
  storage.ts       |   95.07 |    93.44 |   89.47 |   95.07 | ...66-267,270-271 
 ...nfirmation-bus |   98.29 |    97.14 |     100 |   98.29 |                   
  message-bus.ts   |   98.14 |    97.05 |     100 |   98.14 | 42-43             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/core          |   84.59 |    82.72 |   89.05 |   84.59 |                   
  baseLlmClient.ts |   91.63 |    84.37 |   84.61 |   91.63 | ...91,299-313,380 
  client.ts        |   78.45 |    77.21 |   85.18 |   78.45 | ...1508,1545-1548 
  ...tGenerator.ts |    72.1 |    61.11 |     100 |    72.1 | ...63,365,372-375 
  ...lScheduler.ts |   81.13 |    82.25 |   93.33 |   81.13 | ...2332,2384-2388 
  geminiChat.ts    |   88.81 |    84.36 |    87.5 |   88.81 | ...1304,1371-1372 
  geminiRequest.ts |     100 |      100 |     100 |     100 |                   
  ...htProtocol.ts |    9.09 |      100 |       0 |    9.09 | 34-42,45-49,52-87 
  logger.ts        |   87.33 |    87.02 |     100 |   87.33 | ...61-565,611-625 
  ...tyDefaults.ts |     100 |      100 |     100 |     100 |                   
  ...olExecutor.ts |   92.59 |       75 |      50 |   92.59 | 41-42             
  ...on-helpers.ts |   85.71 |    70.58 |     100 |   85.71 | ...90-191,205-214 
  ...issionFlow.ts |   98.59 |    94.73 |     100 |   98.59 | 93                
  prompts.ts       |   89.16 |    86.41 |   76.92 |   89.16 | ...-965,1168-1169 
  tokenLimits.ts   |     100 |    89.47 |     100 |     100 | 51-52             
  ...okTriggers.ts |   99.31 |    90.41 |     100 |   99.31 | 124,135           
  turn.ts          |   96.42 |    88.88 |     100 |   96.42 | ...00,413-414,462 
 ...ntentGenerator |   95.12 |    81.91 |   93.61 |   95.12 |                   
  ...tGenerator.ts |   97.13 |    83.58 |    92.3 |   97.13 | ...22,714,870,926 
  converter.ts     |   94.51 |    80.62 |     100 |   94.51 | ...06-607,617,816 
  index.ts         |       0 |        0 |       0 |       0 | 1-21              
 ...ntentGenerator |   91.53 |    71.64 |   93.33 |   91.53 |                   
  ...tGenerator.ts |      90 |    70.96 |   92.85 |      90 | ...80-286,304-305 
  index.ts         |     100 |       80 |     100 |     100 | 50                
 ...ntentGenerator |   93.33 |    83.33 |      90 |   93.33 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tGenerator.ts |   93.31 |    83.33 |      90 |   93.31 | ...94,804-805,833 
 ...ntentGenerator |   80.52 |    84.56 |   89.61 |   80.52 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  converter.ts     |   76.76 |    82.08 |    87.5 |   76.76 | ...1575,1596-1602 
  errorHandler.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |   52.38 |    44.44 |      50 |   52.38 | ...77,81-85,89-93 
  ...tGenerator.ts |   48.78 |    91.66 |   77.77 |   48.78 | ...10-163,166-167 
  pipeline.ts      |   93.65 |     84.9 |     100 |   93.65 | ...79-480,488,553 
  ...ureContext.ts |     100 |      100 |     100 |     100 |                   
  ...ingOptions.ts |       0 |        0 |       0 |       0 | 1                 
  ...CallParser.ts |   90.66 |    88.57 |     100 |   90.66 | ...15-319,349-350 
  ...kingParser.ts |     100 |    96.87 |     100 |     100 | 42                
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...rator/provider |   96.56 |    88.46 |   95.45 |   96.56 |                   
  dashscope.ts     |   97.02 |    88.15 |   93.33 |   97.02 | ...37-238,314-315 
  deepseek.ts      |   95.55 |    90.56 |     100 |   95.55 | ...31-132,145-146 
  default.ts       |   94.62 |    86.36 |   85.71 |   94.62 | 85-86,156-158     
  index.ts         |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  mistral.ts       |   96.07 |    73.33 |     100 |   96.07 | 32-33             
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  openrouter.ts    |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 |                   
 src/extension     |   60.56 |    79.46 |    78.4 |   60.56 |                   
  ...-converter.ts |   62.35 |    47.82 |      90 |   62.35 | ...90-791,800-832 
  ...ionManager.ts |   47.04 |    82.06 |    65.9 |   47.04 | ...1398,1408-1427 
  ...onSettings.ts |   93.46 |    93.05 |     100 |   93.46 | ...17-221,228-232 
  ...-converter.ts |   54.88 |    94.44 |      60 |   54.88 | ...35-146,158-192 
  github.ts        |   44.94 |    88.52 |      60 |   44.94 | ...53-359,398-451 
  index.ts         |     100 |      100 |     100 |     100 |                   
  marketplace.ts   |   97.29 |    93.75 |     100 |   97.29 | ...64,184-185,274 
  npm.ts           |   48.66 |    76.08 |      75 |   48.66 | ...18-420,427-431 
  override.ts      |   94.11 |    88.88 |     100 |   94.11 | 63-64,81-82       
  settings.ts      |   66.26 |      100 |      50 |   66.26 | 81-108,143-149    
  storage.ts       |     100 |      100 |     100 |     100 |                   
  ...ableSchema.ts |     100 |      100 |     100 |     100 |                   
  variables.ts     |   88.75 |    83.33 |     100 |   88.75 | ...28-231,234-237 
 src/followup      |   46.91 |     92.3 |   71.87 |   46.91 |                   
  followupState.ts |      96 |    89.74 |     100 |      96 | 159-161,218-219   
  index.ts         |     100 |      100 |     100 |     100 |                   
  overlayFs.ts     |   95.06 |       84 |     100 |   95.06 | 78,108,122,133    
  speculation.ts   |   13.22 |      100 |   16.66 |   13.22 | 88-458,518-568    
  ...onToolGate.ts |     100 |    96.29 |     100 |     100 | 93                
  ...nGenerator.ts |    38.4 |    95.12 |   33.33 |    38.4 | ...16-318,353-383 
 src/generated     |       0 |        0 |       0 |       0 |                   
  git-commit.ts    |       0 |        0 |       0 |       0 | 1-10              
 src/hooks         |   80.63 |    84.35 |   84.16 |   80.63 |                   
  ...okRegistry.ts |   86.48 |    77.08 |     100 |   86.48 | ...41-344,362-369 
  ...bortSignal.ts |     100 |      100 |     100 |     100 |                   
  ...terpolator.ts |   96.66 |    93.33 |     100 |   96.66 | 66-67             
  ...HookRunner.ts |   96.68 |    87.23 |     100 |   96.68 | 110-112,231-233   
  ...Aggregator.ts |   96.37 |    90.54 |     100 |   96.37 | ...89,291-292,365 
  ...entHandler.ts |   95.58 |    84.37 |   92.59 |   95.58 | ...29,682-683,693 
  hookPlanner.ts   |   84.13 |    76.59 |      90 |   84.13 | ...38,144,162-173 
  hookRegistry.ts  |   88.83 |    86.36 |     100 |   88.83 | ...21,326,330,334 
  hookRunner.ts    |   53.94 |     72.6 |   61.11 |   53.94 | ...27-728,737-738 
  hookSystem.ts    |   75.47 |      100 |   56.41 |   75.47 | ...75-576,582-583 
  ...HookRunner.ts |   75.51 |     61.9 |      80 |   75.51 | ...05-406,424-425 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...SkillHooks.ts |   78.75 |       75 |   66.66 |   78.75 | 62-66,137-152     
  ...oksManager.ts |    96.5 |     91.8 |     100 |    96.5 | ...90,209-210,223 
  ssrfGuard.ts     |   77.22 |    85.36 |     100 |   77.22 | ...57,261-267,273 
  trustedHooks.ts  |       0 |        0 |       0 |       0 | 1-124             
  types.ts         |   90.18 |    90.78 |   85.18 |   90.18 | ...91-392,452-456 
  urlValidator.ts  |     100 |      100 |     100 |     100 |                   
 src/ide           |   74.28 |    83.39 |   78.33 |   74.28 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  detect-ide.ts    |     100 |      100 |     100 |     100 |                   
  ide-client.ts    |    64.2 |    81.48 |   66.66 |    64.2 | ...9-970,999-1007 
  ide-installer.ts |   89.06 |    79.31 |     100 |   89.06 | ...36,143-147,160 
  ideContext.ts    |     100 |      100 |     100 |     100 |                   
  process-utils.ts |   84.84 |    71.79 |     100 |   84.84 | ...37,151,193-194 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/lsp           |   33.92 |    45.16 |   45.76 |   33.92 |                   
  ...nfigLoader.ts |   70.27 |    35.89 |   94.73 |   70.27 | ...20-422,426-432 
  ...ionFactory.ts |    4.29 |      100 |       0 |    4.29 | ...20-371,377-394 
  ...Normalizer.ts |   23.09 |    13.72 |   30.43 |   23.09 | ...04-905,909-924 
  ...verManager.ts |   13.52 |    81.25 |   29.16 |   13.52 | ...75-694,700-730 
  ...eLspClient.ts |   17.89 |      100 |       0 |   17.89 | ...37-244,254-258 
  ...LspService.ts |   45.87 |    62.13 |   66.66 |   45.87 | ...1282,1299-1309 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mcp           |   78.69 |    75.34 |   75.92 |   78.69 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...h-provider.ts |   86.95 |      100 |   33.33 |   86.95 | ...,93,97,101-102 
  ...h-provider.ts |   73.82 |    53.92 |     100 |   73.82 | ...88-895,902-904 
  ...en-storage.ts |   98.62 |    97.72 |     100 |   98.62 | 87-88             
  oauth-utils.ts   |   70.58 |    85.29 |    90.9 |   70.58 | ...70-290,315-344 
  ...n-provider.ts |   89.83 |    95.83 |   45.45 |   89.83 | ...43,147,151-152 
 .../token-storage |   79.52 |    86.66 |   86.36 |   79.52 |                   
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   82.87 |    82.35 |   92.85 |   82.87 | ...63-173,181-182 
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   68.14 |    82.35 |   64.28 |   68.14 | ...81-295,298-314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/memory        |   67.44 |       76 |   65.62 |   67.44 |                   
  const.ts         |     100 |      100 |     100 |     100 |                   
  dream.ts         |   65.65 |    73.33 |      50 |   65.65 | 50,107-148        
  ...entPlanner.ts |   57.84 |    72.72 |   33.33 |   57.84 | ...35,140-147,152 
  entries.ts       |   63.77 |    79.16 |      50 |   63.77 | ...72-180,183-189 
  extract.ts       |    95.2 |    79.16 |     100 |    95.2 | 81-86,125         
  ...entPlanner.ts |   63.08 |    65.71 |   41.17 |   63.08 | ...17,222-223,332 
  ...ionPlanner.ts |       0 |        0 |       0 |       0 | 1                 
  forget.ts        |    45.8 |    61.53 |   44.44 |    45.8 | ...04,211,214-346 
  indexer.ts       |   83.87 |    45.45 |     100 |   83.87 | ...50,56-57,69-70 
  manager.ts       |   75.31 |    81.04 |    75.6 |   75.31 | ...1278,1291-1293 
  memoryAge.ts     |   90.47 |    77.77 |     100 |   90.47 | 50-51             
  paths.ts         |   55.47 |    89.47 |   85.71 |   55.47 | ...,89-90,106-114 
  prompt.ts        |   93.36 |    71.42 |     100 |   93.36 | ...58,161,228-229 
  recall.ts        |   79.56 |    69.38 |   88.88 |   79.56 | ...40-245,269-280 
  ...ceSelector.ts |   91.95 |    77.27 |     100 |   91.95 | ...08,110-111,119 
  scan.ts          |   87.91 |    68.42 |     100 |   87.91 | ...47-48,58,82-87 
  ...entPlanner.ts |    11.5 |      100 |       0 |    11.5 | ...57-192,210-298 
  status.ts        |   10.52 |      100 |       0 |   10.52 | 41-98             
  store.ts         |   94.44 |    83.33 |     100 |   94.44 | 56-57,92-93       
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mocks         |       0 |        0 |       0 |       0 |                   
  msw.ts           |       0 |        0 |       0 |       0 | 1-9               
 src/models        |   89.31 |    85.47 |    87.5 |   89.31 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...tor-config.ts |   90.24 |    91.42 |     100 |   90.24 | 142,148,151-160   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nfigErrors.ts |   74.22 |       44 |   84.61 |   74.22 | ...,67-74,106-117 
  ...igResolver.ts |   98.63 |    92.53 |     100 |   98.63 | 161,323,329       
  modelRegistry.ts |     100 |    98.59 |     100 |     100 | 222               
  modelsConfig.ts  |   84.57 |    81.92 |   81.57 |   84.57 | ...1223,1252-1253 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/output        |     100 |      100 |     100 |     100 |                   
  ...-formatter.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/permissions   |   71.18 |    88.73 |   48.57 |   71.18 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...on-manager.ts |   81.42 |    86.66 |      80 |   81.42 | ...29-830,837-846 
  rule-parser.ts   |   95.99 |    93.18 |     100 |   95.99 | ...-864,1013-1015 
  ...-semantics.ts |   58.28 |    85.27 |    30.2 |   58.28 | ...1604-1614,1643 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/prompts       |   83.63 |      100 |    87.5 |   83.63 |                   
  mcp-prompts.ts   |   18.18 |      100 |       0 |   18.18 | 11-19             
  ...t-registry.ts |     100 |      100 |     100 |     100 |                   
 src/qwen          |   86.01 |    79.48 |   97.18 |   86.01 |                   
  ...tGenerator.ts |   98.64 |    98.18 |     100 |   98.64 | 105-106           
  qwenOAuth2.ts    |   84.99 |    74.81 |   93.33 |   84.99 | ...,985-1001,1031 
  ...kenManager.ts |   83.76 |    76.22 |     100 |   83.76 | ...62-767,788-793 
 src/services      |   86.91 |    85.03 |   90.09 |   86.91 |                   
  ...ionTrailer.ts |     100 |      100 |     100 |     100 |                   
  ...llRegistry.ts |   97.82 |    94.73 |     100 |   97.82 | 172-173           
  ...ionService.ts |   95.53 |    95.14 |     100 |   95.53 | ...92,354,356-360 
  ...ingService.ts |    84.1 |    84.35 |   82.85 |    84.1 | ...1240,1257-1258 
  ...ttribution.ts |   91.73 |    87.71 |      90 |   91.73 | ...80-685,826-827 
  cronScheduler.ts |   97.56 |    92.98 |     100 |   97.56 | 62-63,77,155      
  ...eryService.ts |   80.43 |    95.45 |      75 |   80.43 | ...19-134,140-141 
  fileReadCache.ts |     100 |      100 |     100 |     100 |                   
  ...temService.ts |   89.76 |     85.1 |   88.88 |   89.76 | ...89,191,266-273 
  ...ratedFiles.ts |      96 |    88.23 |     100 |      96 | 119-120,146-147   
  gitInit.ts       |     100 |      100 |     100 |     100 |                   
  gitService.ts    |   68.75 |     92.3 |   55.55 |   68.75 | ...12-122,125-129 
  ...reeService.ts |   71.83 |    68.47 |    91.3 |   71.83 | ...89-790,806,822 
  ...ionService.ts |   98.13 |     97.8 |   95.45 |   98.13 | ...32-333,380-381 
  ...orRegistry.ts |   96.34 |    91.66 |     100 |   96.34 | ...90-391,542-543 
  sessionRecap.ts  |   12.34 |      100 |       0 |   12.34 | 49-158            
  ...ionService.ts |   90.05 |    79.14 |   96.55 |   90.05 | ...1272,1276-1277 
  sessionTitle.ts  |   93.91 |    71.15 |     100 |   93.91 | ...34-237,268-269 
  ...ionService.ts |   83.01 |    78.66 |   87.75 |   83.01 | ...1482,1488-1493 
  ...UseSummary.ts |   94.63 |    88.67 |     100 |   94.63 | ...69-171,221-222 
 ...icrocompaction |   98.62 |    86.44 |     100 |   98.62 |                   
  microcompact.ts  |   98.62 |    86.44 |     100 |   98.62 | 138,142           
 src/skills        |    87.5 |     83.8 |   94.23 |    87.5 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...activation.ts |     100 |     93.1 |     100 |     100 | 93,112            
  skill-load.ts    |   92.94 |    81.63 |     100 |   92.94 | ...06,226,238-240 
  skill-manager.ts |   83.31 |    79.66 |   90.32 |   83.31 | ...1115,1122-1126 
  skill-paths.ts   |   86.74 |    77.77 |     100 |   86.74 | ...00-101,106-107 
  symlinkScope.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/subagents     |   82.84 |    79.74 |   95.23 |   82.84 |                   
  ...tin-agents.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...-selection.ts |     100 |      100 |     100 |     100 |                   
  ...nt-manager.ts |   76.74 |    71.42 |   92.85 |   76.74 | ...1155,1177-1178 
  types.ts         |     100 |      100 |     100 |     100 |                   
  validation.ts    |   92.46 |    95.18 |     100 |   92.46 | 51-56,69-74,78-83 
 src/telemetry     |    73.5 |    85.47 |   77.56 |    73.5 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...-exporters.ts |   46.37 |      100 |   44.44 |   46.37 | ...85,88-89,92-93 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-111             
  ...-processor.ts |   93.89 |    90.21 |   94.11 |   93.89 | ...70-275,294-295 
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-128             
  loggers.ts       |    51.9 |       64 |   57.77 |    51.9 | ...1214,1231-1251 
  metrics.ts       |    74.9 |    82.95 |   74.54 |    74.9 | ...58-978,981-992 
  sanitize.ts      |      80 |    83.33 |     100 |      80 | 35-36,41-42       
  sdk.ts           |   90.42 |    83.56 |   76.92 |   90.42 | ...16-317,337-341 
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...on-tracing.ts |   92.77 |     87.3 |     100 |   92.77 | 79-93,379-383     
  ...etry-utils.ts |     100 |      100 |     100 |     100 |                   
  ...l-decision.ts |     100 |      100 |     100 |     100 |                   
  ...e-id-utils.ts |     100 |      100 |     100 |     100 |                   
  tracer.ts        |   99.24 |    88.88 |     100 |   99.24 | 53                
  types.ts         |   79.17 |    85.83 |   83.33 |   79.17 | ...1149,1152-1181 
  uiTelemetry.ts   |   92.97 |    96.96 |   81.25 |   92.97 | ...93-194,200-207 
 ...ry/qwen-logger |   68.24 |    79.56 |   64.91 |   68.24 |                   
  event-types.ts   |       0 |        0 |       0 |       0 |                   
  qwen-logger.ts   |   68.24 |    79.34 |   64.28 |   68.24 | ...1055,1093-1094 
 src/test-utils    |   93.16 |    95.83 |   73.52 |   93.16 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  ...st-helpers.ts |   94.11 |       90 |     100 |   94.11 | 69-70             
  index.ts         |     100 |      100 |     100 |     100 |                   
  mock-tool.ts     |   91.19 |    97.05 |   68.96 |   91.19 | ...38,202-203,216 
  ...aceContext.ts |     100 |      100 |     100 |     100 |                   
 src/tools         |   77.07 |    81.48 |   85.59 |   77.07 |                   
  ...erQuestion.ts |   88.93 |    76.74 |    90.9 |   88.93 | ...39-340,347-348 
  cron-create.ts   |   97.75 |    88.88 |   83.33 |   97.75 | 30-31             
  cron-delete.ts   |   96.82 |      100 |   83.33 |   96.82 | 26-27             
  cron-list.ts     |   96.66 |      100 |   83.33 |   96.66 | 25-26             
  diffOptions.ts   |     100 |      100 |     100 |     100 |                   
  edit.ts          |   78.01 |    84.76 |   73.33 |   78.01 | ...86-687,774-824 
  exitPlanMode.ts  |   85.09 |    85.71 |     100 |   85.09 | ...60-163,177-189 
  glob.ts          |   90.63 |    88.33 |   84.61 |   90.63 | ...28,171,302,305 
  grep.ts          |   79.19 |    85.71 |   78.94 |   79.19 | ...20,560,569-576 
  ls.ts            |   96.74 |    90.27 |     100 |   96.74 | 176-181,212,216   
  lsp.ts           |   72.77 |    60.09 |   90.32 |   72.77 | ...1211,1213-1214 
  ...nt-manager.ts |   69.73 |    75.29 |   71.42 |   69.73 | ...29-732,749-786 
  mcp-client.ts    |   33.18 |    77.41 |   66.66 |   33.18 | ...1490,1494-1497 
  mcp-tool.ts      |   90.98 |    88.88 |   96.42 |   90.98 | ...95-596,646-647 
  memory-config.ts |       0 |        0 |       0 |       0 | 1-47              
  ...iable-tool.ts |     100 |    84.61 |     100 |     100 | 102,109           
  monitor.ts       |   92.27 |    83.94 |      92 |   92.27 | ...18,547-550,563 
  ...nforcement.ts |   82.44 |       90 |     100 |   82.44 | 174-185,234-247   
  read-file.ts     |   95.07 |     88.6 |      90 |   95.07 | ...99,290-293,296 
  ripGrep.ts       |   94.59 |    85.71 |   93.33 |   94.59 | ...60,463,541-542 
  ...-transport.ts |    6.34 |        0 |       0 |    6.34 | 47-145            
  send-message.ts  |   89.32 |    91.66 |   83.33 |   89.32 | 44-45,68-76       
  shell.ts         |   72.18 |    80.23 |   89.65 |   72.18 | ...3659,3708-3714 
  skill-utils.ts   |     100 |      100 |     100 |     100 |                   
  skill.ts         |   88.11 |    91.17 |   84.61 |   88.11 | ...95,399,422-444 
  ...eticOutput.ts |   95.12 |      100 |      80 |   95.12 | 87-88             
  task-stop.ts     |   93.14 |    96.15 |   85.71 |   93.14 | 39-40,54-64       
  todoWrite.ts     |   85.42 |    84.09 |   84.61 |   85.42 | ...05-410,432-433 
  tool-error.ts    |     100 |      100 |     100 |     100 |                   
  tool-names.ts    |     100 |      100 |     100 |     100 |                   
  tool-registry.ts |   74.79 |       75 |   80.48 |   74.79 | ...92-793,801-802 
  tool-search.ts   |   95.19 |    86.48 |    92.3 |   95.19 | ...47-153,208-213 
  tools.ts         |   87.76 |       90 |   88.23 |   87.76 | ...50-451,467-473 
  web-fetch.ts     |   88.59 |    79.48 |    92.3 |   88.59 | ...12-313,315-316 
  write-file.ts    |    79.2 |    79.26 |   83.33 |    79.2 | ...39-642,654-689 
 src/tools/agent   |   83.39 |    84.95 |   83.92 |   83.39 |                   
  agent.ts         |   83.69 |    85.38 |   84.31 |   83.69 | ...1668,1677-1681 
  fork-subagent.ts |   78.26 |    71.42 |      80 |   78.26 | 54-72,104-105     
 src/utils         |   88.65 |    87.13 |   93.19 |   88.65 |                   
  LruCache.ts      |       0 |        0 |       0 |       0 | 1-41              
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...cFileWrite.ts |   76.08 |    44.44 |     100 |   76.08 | 61-70,72          
  bareMode.ts      |   27.27 |      100 |       0 |   27.27 | 9-15,18-19        
  browser.ts       |    7.69 |      100 |       0 |    7.69 | 17-56             
  ...igResolver.ts |     100 |      100 |     100 |     100 |                   
  ...engthError.ts |   89.11 |    86.66 |     100 |   89.11 | ...28-129,132-133 
  cronDisplay.ts   |   42.85 |    23.07 |     100 |   42.85 | 26-31,33-45,47-54 
  cronParser.ts    |   89.74 |    85.71 |     100 |   89.74 | ...,63-64,183-186 
  debugLogger.ts   |    95.9 |    93.84 |   94.73 |    95.9 | 106-107,214-218   
  editHelper.ts    |   93.63 |    83.52 |     100 |   93.63 | ...28-429,463-464 
  editor.ts        |   97.61 |    95.71 |     100 |   97.61 | ...70-271,273-274 
  ...arResolver.ts |   94.28 |    88.88 |     100 |   94.28 | 28-29,125-126     
  ...entContext.ts |     100 |    95.45 |     100 |     100 | 83                
  errorParsing.ts  |    97.7 |    97.05 |     100 |    97.7 | 72-73             
  ...rReporting.ts |   88.46 |       90 |     100 |   88.46 | 69-74             
  errors.ts        |   70.92 |    79.59 |   53.33 |   70.92 | ...03-219,223-229 
  fetch.ts         |   70.18 |    71.42 |   71.42 |   70.18 | ...42,148,161,186 
  fileUtils.ts     |   91.41 |    86.13 |      95 |   91.41 | ...1182,1186-1192 
  forkedAgent.ts   |    78.5 |    70.73 |   85.71 |    78.5 | ...30-436,441-447 
  formatters.ts    |   54.54 |       50 |     100 |   54.54 | 12-16             
  ...eUtilities.ts |   89.21 |    86.66 |     100 |   89.21 | 16-17,49-55,65-66 
  ...rStructure.ts |   94.36 |    94.28 |     100 |   94.36 | ...17-120,330-335 
  getPty.ts        |    12.5 |      100 |       0 |    12.5 | 21-34             
  gitDiff.ts       |   92.36 |    79.53 |     100 |   92.36 | ...55-856,928-929 
  ...noreParser.ts |    92.3 |    89.36 |     100 |    92.3 | ...15-116,186-187 
  gitUtils.ts      |   56.66 |    85.71 |      75 |   56.66 | ...2,72-73,97-148 
  iconvHelper.ts   |     100 |      100 |     100 |     100 |                   
  ...rePatterns.ts |     100 |      100 |     100 |     100 |                   
  ...ionManager.ts |     100 |     90.9 |     100 |     100 | 26                
  ...lPromptIds.ts |     100 |      100 |     100 |     100 |                   
  jsonl-utils.ts   |    74.1 |    90.76 |   58.33 |    74.1 | ...23-326,336-342 
  ...-detection.ts |     100 |      100 |     100 |     100 |                   
  ...yDiscovery.ts |    83.9 |    79.36 |     100 |    83.9 | ...16,319,411-414 
  ...tProcessor.ts |   93.63 |       90 |     100 |   93.63 | ...96-302,384-385 
  ...Inspectors.ts |   61.53 |      100 |      50 |   61.53 | 18-23             
  ...kerChecker.ts |   82.55 |    78.57 |     100 |   82.55 | 68-69,79-84,92-98 
  notebook.ts      |   94.35 |    84.78 |     100 |   94.35 | ...10,122,174-176 
  openaiLogger.ts  |   87.93 |    82.85 |     100 |   87.93 | ...22-124,147-152 
  partUtils.ts     |     100 |      100 |     100 |     100 |                   
  pathReader.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |   93.21 |    91.86 |     100 |   93.21 | ...89-390,392-394 
  pdf.ts           |   93.68 |    87.05 |     100 |   93.68 | ...96-297,321-325 
  projectPath.ts   |     100 |      100 |     100 |     100 |                   
  ...ectSummary.ts |   89.39 |    72.41 |     100 |   89.39 | ...37-142,193-196 
  ...tIdContext.ts |     100 |      100 |     100 |     100 |                   
  proxyUtils.ts    |     100 |      100 |     100 |     100 |                   
  ...rDetection.ts |   58.57 |       76 |     100 |   58.57 | ...4,88-89,95-100 
  ...noreParser.ts |   85.45 |    85.18 |     100 |   85.45 | ...59,65-66,72-73 
  rateLimit.ts     |   92.55 |    85.92 |     100 |   92.55 | ...70-272,309-310 
  readManyFiles.ts |   87.96 |    86.95 |     100 |   87.96 | ...05-207,223-234 
  retry.ts         |   89.81 |    88.05 |     100 |   89.81 | ...29,350,357-358 
  ripgrepUtils.ts  |   46.53 |    84.37 |   66.66 |   46.53 | ...32-233,245-322 
  ...sDiscovery.ts |   97.42 |    92.85 |     100 |   97.42 | ...04,182-183,202 
  ...tchOptions.ts |   63.85 |    64.28 |   83.33 |   63.85 | ...29-130,187-188 
  runtimeStatus.ts |   85.58 |    82.05 |     100 |   85.58 | ...81,231-237,239 
  safeJsonParse.ts |   74.07 |    83.33 |     100 |   74.07 | 40-46             
  ...nStringify.ts |     100 |      100 |     100 |     100 |                   
  ...aConverter.ts |   90.78 |    88.23 |     100 |   90.78 | ...41-42,93,95-96 
  ...aValidator.ts |   94.57 |    80.26 |     100 |   94.57 | ...04,213-216,270 
  ...r-launcher.ts |   76.92 |     91.3 |   66.66 |   76.92 | ...34,136,157-195 
  ...orageUtils.ts |   96.89 |    85.84 |     100 |   96.89 | ...51,367,447,466 
  shell-utils.ts   |   82.93 |    89.55 |     100 |   82.93 | ...1522,1529-1533 
  ...lAstParser.ts |   95.58 |    85.79 |     100 |   95.58 | ...1059-1061,1071 
  ...nlyChecker.ts |   95.75 |    92.39 |     100 |   95.75 | ...00-301,313-314 
  sideQuery.ts     |   98.71 |    97.14 |     100 |   98.71 | 106               
  ...pEventSink.ts |     100 |       80 |     100 |     100 | 61                
  ...tGenerator.ts |     100 |      100 |     100 |     100 |                   
  ...ameContext.ts |     100 |      100 |     100 |     100 |                   
  symlink.ts       |   77.77 |       50 |     100 |   77.77 | 44,54-59          
  ...emEncoding.ts |   96.36 |    91.17 |     100 |   96.36 | 59-60,124-125     
  terminalSafe.ts  |     100 |      100 |     100 |     100 |                   
  ...Serializer.ts |   98.72 |       90 |     100 |   98.72 | 42-43,134,201-203 
  testUtils.ts     |   53.33 |      100 |   33.33 |   53.33 | ...53,59-64,70-72 
  textUtils.ts     |      60 |      100 |   66.66 |      60 | 36-55             
  thoughtUtils.ts  |     100 |    92.85 |     100 |     100 | 71                
  ...-converter.ts |   94.59 |    85.71 |     100 |   94.59 | 35-36             
  tool-utils.ts    |    93.6 |     91.3 |     100 |    93.6 | ...58-159,162-163 
  truncation.ts    |     100 |       92 |     100 |     100 | 52,71             
  windowsPath.ts   |   89.47 |    79.31 |     100 |   89.47 | ...57-58,62,90-91 
  ...aceContext.ts |   93.71 |    89.28 |   93.33 |   93.71 | ...24-225,249-251 
  xml.ts           |     100 |      100 |     100 |     100 |                   
  yaml-parser.ts   |      92 |    84.31 |     100 |      92 | 49-53,65-69       
 ...ils/filesearch |   85.77 |    81.06 |   96.42 |   85.77 |                   
  crawlCache.ts    |     100 |      100 |     100 |     100 |                   
  crawler.ts       |   82.84 |    77.49 |   94.82 |   82.84 | ...1451,1485-1486 
  fileSearch.ts    |   93.58 |    87.32 |     100 |   93.58 | ...46-247,249-250 
  ignore.ts        |     100 |      100 |     100 |     100 |                   
  result-cache.ts  |     100 |     92.3 |     100 |     100 | 46                
 ...uest-tokenizer |   56.63 |    74.52 |   74.19 |   56.63 |                   
  ...eTokenizer.ts |   41.86 |    76.47 |   69.23 |   41.86 | ...70-443,453-507 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tTokenizer.ts |   68.39 |    69.49 |    90.9 |   68.39 | ...24-325,327-328 
  ...ageFormats.ts |      76 |      100 |   33.33 |      76 | 45-48,55-56       
  textTokenizer.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
-------------------|---------|----------|---------|---------|-------------------

For detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run.

chiga0 pushed a commit that referenced this pull request May 11, 2026
…ntion sections

Restructures the PR rollout based on baseline-informed re-prioritization:
- PR-A = original PR4 (progressive MCP). Ship first — biggest user-facing
  improvement (7-10s → 340ms for MCP users) and the only PR with behavioral
  semantic change, so isolating it from other refactors makes the audit
  trail cleaner.
- PR-B = original PR2 (loadSettingsAsync) + PR3 (parallel init + lazy entry).
  Merged because PR2's measured win shrank to ~3ms (baseline showed
  after_load_settings at 9ms warm-cache, not the 100-200ms estimated in the
  original design). Folding it into PR3's "startup main path optimization"
  narrative keeps review cost low and avoids a near-empty standalone PR.
- PR0+1 (#3994) remains the hard prerequisite for both.

New sections in design.md § 4:
- § 4.0  Label mapping: old PR2/3/4 → new PR-A/PR-B-α/PR-B-β so existing
  decision-log references stay traceable without mechanical rewrites.
- § 4.1  Restructured rollout with the "why this order" rationale.
- § 4.2  PR inter-dependency matrix. PR-A and PR-B have no logical
  dependencies on each other; they share AppContainer.tsx / gemini.tsx
  edits so serial merge is recommended to avoid rebase churn.
- § 4.3  UX / breaking-change matrix per PR. Explicit audit checklist for
  PR-A (grep all `MCPDiscoveryState.COMPLETED` and `config.initialize`
  call sites + integration tests + release notes).
- § 4.4  Bundle size impact. PR-A negligible; PR-B headless chunk
  predicted -30~50% (Ink + AppContainer + themes + hooks剥离), with
  scripts/check-bundle-leakage.mjs as the CI gate.
- § 4.5  PR0+1 retention strategy. Documents that production code, scripts,
  fixtures, summary.json baselines, Heisenberg report, node-pty devDep,
  and design docs all stay on main; only raw.jsonl + report.md are
  gitignored. Avoids splitting "infrastructure" from "feature" PRs.

§§ 6-8 headers updated to PR-B Phase α / PR-B Phase β / PR-A labels with
cross-references to § 4 for context.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@chiga0
chiga0 force-pushed the feat/first-screen-performance-optimization branch from 3fe5351 to 322aaa9 Compare May 11, 2026 03:18
@chiga0
chiga0 force-pushed the feat/first-screen-performance-optimization branch from 322aaa9 to 68486b3 Compare May 11, 2026 03:34
@chiga0 chiga0 changed the title feat(perf): first-screen / startup observability baseline (PR0+1) feat(perf): progressive MCP availability — MCP no longer blocks first input May 11, 2026
@chiga0
chiga0 force-pushed the feat/first-screen-performance-optimization branch 3 times, most recently from 97eff9c to c8eef71 Compare May 11, 2026 05:12
@chiga0
chiga0 marked this pull request as ready for review May 11, 2026 05:12
… input

Today `Config.initialize()` runs MCP discovery synchronously and the cli
can't accept input until every configured MCP server finishes its
discover handshake. One slow or hung server bottlenecks every user with
MCP configured. Validated by the profiler instrumentation added in this
PR (set `QWEN_CODE_PROFILE_STARTUP=1` to reproduce):

| User scenario             | Time to first prompt input |
| ------------------------- | -------------------------- |
| No MCP                    | ~480 ms                    |
| 1 fast MCP                | ~875 ms                    |
| 2 fast + 1 slow MCP       | **~7.1 s**                 |
| 1 hung MCP server         | **~10.5 s**                |

(Measured on macOS arm64 / Node 24.15, n=30/fixture, p50.)

`Config.initialize()` now passes `{ skipDiscovery: true }` to
`createToolRegistry` by default and kicks off MCP discovery in a
fire-and-forget background path. As each server completes discover,
the cli's `AppContainer` debounces `setTools()` calls into one-frame
(16 ms) batches so the model sees the consolidated tool list shortly
after each server settles. Rollback: `QWEN_CODE_LEGACY_MCP_BLOCKING=1`.

- `packages/core/src/config/config.ts` — `Config.initialize` switches
  to `skipDiscovery: true` + new `startMcpDiscoveryInBackground()`
  (defensive against partially-stubbed `ToolRegistry` in tests). Adds
  `MCPServerConfig.discoveryTimeoutMs` (last positional ctor param —
  doesn't shift existing call sites). Tool-call timeout is untouched.
- `packages/core/src/tools/tool-registry.ts` — new
  `getMcpClientManager()` getter so the background path can call the
  incremental discover directly without going through `discoverMcpTools`
  (which would wipe already-registered tools).
- `packages/core/src/tools/mcp-client-manager.ts` —
  `discoverAllMcpToolsIncremental` now: emits `mcp-client-update`
  after IN_PROGRESS transition, wraps each per-server discover in a
  discovery-only timeout (stdio 30s, remote 5s), emits trailing
  `mcp-client-update` after COMPLETED so UI subscribers see the
  terminal state.
- `packages/cli/src/ui/AppContainer.tsx` — new `useEffect` (gated on
  `isConfigInitialized`) subscribes to `mcp-client-update` and
  16ms-batches `setTools()` calls. Same effect also defers
  `finalizeStartupProfile` until MCP settles (or 35s hard cap), so
  startup-perf profiles capture the full MCP timeline.

Activated only by `QWEN_CODE_PROFILE_STARTUP=1`; when unset every
profiler entry point short-circuits in a single null/flag check and
returns. Heisenberg overhead measured at -1.12% Δp50 between
profile-on vs profile-off (Welch p=0.092, n=30/config × 3 configs) —
within statistical noise.

- `packages/cli/src/utils/startupProfiler.ts` — extended with
  `events` array (multi-fire), `recordStartupEvent`,
  `setInteractiveMode`, `derivedPhases`, per-checkpoint heap snapshots,
  `MAX_EVENTS` cap, and `QWEN_CODE_PROFILE_STARTUP_OUTER` / NO_HEAP
  env opt-ins. + 7 new tests.
- `packages/core/src/utils/startupEventSink.ts` (new) — minimal
  cross-package sink so `core` can emit profiler events without
  reverse-depending on `cli`. No-op when no sink registered. + 4 tests.
- `packages/core/src/index.ts` — export `setStartupEventSink` /
  `recordStartupEvent` / type aliases.
- `packages/cli/src/gemini.tsx` — registers the sink at `main()`
  entry, adds `first_paint` checkpoint after Ink render, calls
  `setInteractiveMode(true)` in the interactive branch.
- `packages/core/src/config/config.ts` — emits
  `tool_registry_created`.
- `packages/core/src/core/client.ts` — emits `gemini_tools_updated`
  at the end of `setTools()`.
- `packages/core/src/tools/mcp-client-manager.ts` — emits
  `mcp_discovery_start`, `mcp_server_ready:<name>`,
  `mcp_first_tool_registered`, `mcp_all_servers_settled`.
- `packages/cli/src/ui/AppContainer.tsx` — emits
  `config_initialize_start`, `config_initialize_end`, `input_enabled`.

`Config.initialize()` now returns BEFORE MCP discovery completes.
Things to check:
- Any code path that assumed "after `config.initialize()`, all MCP
  tools exist in the registry" — these will see only built-in tools
  initially; new tools appear via `mcp-client-update` events.
- `MCPDiscoveryState.COMPLETED` is now set asynchronously instead of
  synchronously after `initialize()` resolves.
- Model requests issued before MCP settles see only built-in tools;
  subsequent requests see the full set as servers come online.
- Tests that assert MCP tool count immediately after
  `config.initialize()` should wait for the `mcp-client-update` with
  COMPLETED discoveryState instead.

- 313 impacted-area tests green (config / mcp-client-manager / client
  / startupProfiler 18 / startupEventSink 4).
- `tsc --noEmit` clean for `packages/core` and `packages/cli`.
- `eslint` clean on touched files.
- Manual: `QWEN_CODE_PROFILE_STARTUP=1 SANDBOX=1` interactive run
  produces a JSON profile in `~/.qwen/startup-perf/` containing
  `first_paint`, `config_initialize_start/end`, `input_enabled`,
  MCP per-server events, and `gemini_tools_updated`. See PR
  description's "How to validate" section.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@chiga0
chiga0 force-pushed the feat/first-screen-performance-optimization branch from c8eef71 to 8aaec50 Compare May 11, 2026 07:23
@tanzhenxin tanzhenxin added the type/feature-request New feature or enhancement request label May 11, 2026
startupWarnings: string[],
workspaceRoot: string = process.cwd(),
initializationResult: InitializationResult,
) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] runtime.json sidecar deleted from interactive mode

The writeRuntimeStatus + markRuntimeStatusEnabled block and the writeRuntimeStatus import were removed. runtimeStatusEnabled is now never set to true, making the session-swap sidecar logic in Config.refreshSessionId() dead code.

External integrations (terminal multiplexers, IDE integrations, status daemons) that map PID → session ID via runtime.json silently break. This appears accidental — the removal was in the same diff hunk as profiler additions.

Suggested change
) {
const version = await getCliVersion();
setWindowTitle(basename(workspaceRoot), settings);
// Write a small runtime.json sidecar next to the chat log so external
// tools (terminal multiplexers, IDE integrations, status daemons) can
// map the running PID back to its session id and work directory.
try {
const sessionId = config.getSessionId();
const runtimeStatusPath =
config.storage.getRuntimeStatusPath(sessionId);
await writeRuntimeStatus(runtimeStatusPath, {
sessionId,
workDir: config.getTargetDir(),
qwenVersion: version,
});
config.markRuntimeStatusEnabled();
} catch {
// ignored: best-effort, never block UI startup.
}

— glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed real regression — fixed in 6dcea68 → restored via commit a49b3e2 (also restores markRuntimeStatusEnabled(), which is what arms the Config.refreshSessionId() session-swap refresh path you cited).

Verified the diff against main:

  • writeRuntimeStatus re-imported from @qwen-code/qwen-code-core
  • block re-added in startInteractiveUI between setWindowTitle and installTerminalRedrawOptimizer
  • still wrapped in try/catch so a read-only fs does not block UI startup (matches the original behavior)

Non-interactive paths still never call markRuntimeStatusEnabled, so they won't trample a sibling shell's sidecar on the same session id, which preserves the original design.

new Error(
`MCP server '${serverName}' discovery timed out after ${timeoutMs}ms`,
),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Timed-out MCP server silently registers tools

runWithDiscoveryTimeout rejects on timeout but does NOT cancel the underlying discoverMcpToolsForServer. The background promise continues — if the server eventually responds, its tools are registered into the live toolRegistry and mcp-client-update is emitted. The user sees the server as "failed" but its tools are silently active.

A slow/attacker-controlled MCP server can inject tools into the model's surface after the discovery timeout, including tools that shadow built-in ones.

Suggested change
);
private runWithDiscoveryTimeout<T>(
serverName: string,
serverConfig: MCPServerConfig | undefined,
fn: () => Promise<T>,
): Promise<T> {
const timeoutMs = this.discoveryTimeoutFor(serverConfig);
let timedOut = false;
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
timedOut = true;
// Disconnect the client to prevent silent background tool registration
const client = this.clients.get(serverName);
if (client) {
void client.disconnect().catch(() => {});
}
reject(
new Error(
`MCP server '${serverName}' discovery timed out after ${timeoutMs}ms`,
),
);
}, timeoutMs);
fn().then(
(value) => {
clearTimeout(timer);
if (!timedOut) resolve(value);
},
(err) => {
clearTimeout(timer);
if (!timedOut) reject(err instanceof Error ? err : new Error(String(err)));
},
);
});
}

— glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real and exploitable as you flagged — fixed in 6b07a85 with the same disconnect-on-timeout strategy you suggested.

Implementation differs slightly: I keep a local timedOut flag and silently swallow a late success path so the inner promise can't accidentally re-fire mcp_server_ready:ready / mcp_first_tool_registered after the outer rejection. New test runWithDiscoveryTimeout disconnects the client on timeout to abort silent tool registration pins the disconnect.

Combined with the new discoveryTimeoutMs clamping (next thread) and the websocket transport fix, the worst case is now a 5s remote / 30s stdio bounded window with the client torn down at the boundary — client.connect() throws and the registry never receives any tools.

if (serverConfig?.discoveryTimeoutMs !== undefined) {
return serverConfig.discoveryTimeoutMs;
}
// Remote transports (HTTP/SSE) carry network risk and get a shorter

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] discoveryTimeoutMs accepts 0/negative/Infinity without validation

Values pass through to setTimeout unvalidated. 0 causes immediate timeout for every server; Infinity hangs waitForMcpReady() forever in non-interactive mode. Combined with the silent background registration bug above, discoveryTimeoutMs: 0 makes it reliably exploitable.

Suggested change
// Remote transports (HTTP/SSE) carry network risk and get a shorter
private discoveryTimeoutFor(serverConfig?: MCPServerConfig): number {
if (serverConfig?.discoveryTimeoutMs !== undefined) {
return Math.max(100, Math.min(serverConfig.discoveryTimeoutMs, 300_000));
}
const isRemote = !!(serverConfig?.httpUrl || serverConfig?.url);
return isRemote ? 5_000 : 30_000;
}

— glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6b07a85 with the clamp you suggested (100ms floor, 300_000ms ceiling). Added Number.isFinite so Infinity falls back to the per-transport default rather than passing through.

New test discoveryTimeoutMs is clamped to a minimum and maximum asserts setTimeout is called with the clamped values and never with 0, -5, or 10_000_000.

Comment thread packages/core/src/config/config.ts Outdated
*/
private startMcpDiscoveryInBackground(): void {
const registry = this.toolRegistry as ToolRegistry & {
getMcpClientManager?: () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Type-unsafe cast on getMcpClientManager

Uses (this.toolRegistry as ToolRegistry & { getMcpClientManager?: () => ... }) with optional chaining, even though getMcpClientManager() is now a public method on ToolRegistry (added in this PR). If the method is ever removed or renamed, TypeScript won't flag this call site. AppContainer.tsx calls it directly without the cast, showing inconsistency.

Suggested change
getMcpClientManager?: () => {
const manager = this.toolRegistry.getMcpClientManager();

— glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tightened in a49b3e2 — kept a minimal optional-chain guard but the cast now preserves the typed ReturnType<ToolRegistry['getMcpClientManager']>, so a future rename WILL be flagged at this call site (vs. the previous structurally-typed cast which would have silently accepted any function returning anything).

I considered going all-in on the direct call as you suggested, but several core tests stub ToolRegistry as a plain object via createMockToolRegistry-style fixtures; calling a method that doesn't exist on the stub would crash the init path in tests that don't exercise MCP at all. The defensive guard preserves that ergonomics while still being type-safe to renames.

AppContainer.tsx continues to call it without the cast because it has a real ToolRegistry in scope.

// Terminal and layout hooks
const { columns: terminalWidth, rows: terminalHeight } = useTerminalSize();
const previousTerminalWidthRef = useRef(terminalWidth);
const { stdin, setRawMode } = useStdin();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Terminal resize handler deleted

previousTerminalWidthRef, repaintStaticViewport, and the useEffect that called it on terminalWidth changes were removed. No replacement repaint mechanism exists. Users who resize terminals (tmux splits, fullscreen toggle) will see stale header content.

If this removal was intentional, please add a comment explaining why. Otherwise, restore the resize handler.

— glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed accidental — fixed in 6dcea68. Restored previousTerminalWidthRef, repaintStaticViewport, and the terminalWidth-watching useEffect at their original positions. Verified the diff against main so the restored block matches the pre-PR shape (cursor-to + erase-down, not full clearTerminal — keeping the no-flash optimization).

The new effect runs alongside the existing PTY-resize useEffect at the same terminalWidth dep, so resize ordering is preserved.

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);

/**

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] AppContainer MCP batch-flush useEffect untested

This ~85-line useEffect implements 16ms batched setTools() + deferred finalizeStartupProfile and is the core interactive-mode wiring for progressive MCP availability. It has zero test coverage — no AppContainer.test.tsx exists.

Consider adding tests covering: (a) setTools() called after mcp-client-update + flush window; (b) finalizeStartupProfile on COMPLETED; (c) cap timeout fires even when discovery never completes; (d) cleanup unsubscribes from mcp-client-update.

— glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. I won't add full coverage in this PR pass (a ~85-line effect with timers + event emitter + manager state needs dedicated harness work to avoid flakes), but I added several focused tests at lower layers that pin the contracts this useEffect depends on:

  • discoverAllMcpToolsIncremental enforces a per-server discoveryTimeoutMs — caps server discovery, which keeps the COMPLETED state finite.
  • discoverAllMcpToolsIncremental emits the trailing mcp-client-update after COMPLETED — guarantees the subscriber wakes up at the terminal state (your (b) case).
  • runWithDiscoveryTimeout disconnects the client on timeout — prevents the cap-timeout-fires case from racing setTools().
  • discoverAllMcpToolsIncremental skips servers flagged as disabled — keeps the event volume bounded.

Will track a follow-up issue to add the dedicated useEffect tests for (a) flush coalescing, (b) COMPLETED → finalize ordering (now post-flush, see r3223777822), (c) cap timeout, (d) unsubscribe on unmount.

nonInteractiveConfig,
trimmedInput.length > 0 ? trimmedInput : '',
settings,
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Stream-json path dropped settings argument

The settings parameter was removed from the runNonInteractiveStreamJson call, causing it to fall back to createMinimalSettings() (empty settings). The parallel runNonInteractive path still passes settings. This appears accidental — the removal is in the same hunk as the waitForMcpReady() addition.

Suggested change
);
await runNonInteractiveStreamJson(
nonInteractiveConfig,
trimmedInput.length > 0 ? trimmedInput : '',
settings,
);

— glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed accidental — fixed in a49b3e2. The settings argument is now passed through. Also added a regression guard in gemini.test.tsx:

const [configArg, inputArg, settingsArg] = runStreamJsonSpy.mock.calls[0];
...
expect(settingsArg).toBeDefined();

Without this, stream-json sessions silently fell back to createMinimalSettings() and lost any user-configured permission / approval / hook setup.

@chiga0
chiga0 force-pushed the feat/first-screen-performance-optimization branch from 17d00eb to 8aaec50 Compare May 12, 2026 01:44
// user-perceived metric ("first MCP server is ready").
let firstToolEventFired = false;

// Find servers that are new or have changed configuration

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] discoverAllMcpToolsIncremental doesn't skip disabled MCP servers

The new incremental method populates serversToUpdate from Object.entries(servers) without calling cliConfig.isMcpServerDisabled(name). The existing discoverAllMcpTools correctly skips disabled servers (line ~96: if (cliConfig.isMcpServerDisabled(name)) { ... return; }), but the new method connects and registers tools for servers the user explicitly disabled.

Add if (cliConfig.isMcpServerDisabled(name)) continue; inside the for (const [name] of Object.entries(servers)) loop that populates serversToUpdate.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real bug, confirmed by inspection — fixed in 6b07a85. The exact check you suggested:

if (cliConfig.isMcpServerDisabled(name)) {
  debugLogger.debug(`Skipping disabled MCP server: ${name}`);
  continue;
}

is now in the for (const [name] of Object.entries(servers)) loop that populates serversToUpdate. Mirror of the existing protection in discoverAllMcpTools (line ~102). New test discoverAllMcpToolsIncremental skips servers flagged as disabled regression-pins it.

} catch (error) {
recordStartupEvent(`mcp_server_ready:${name}`, {
outcome: 'failed',
reason: getErrorMessage(error),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] serverDiscoveryPromises stale entry permanently blocks reconnection after timeout

When runWithDiscoveryTimeout fires, the per-server catch block (line 513) does not call this.serverDiscoveryPromises.delete(name). The underlying discoverMcpToolsForServer is still stuck at connect(), so its finally hasn't executed and the Map entry remains. Any subsequent discoverMcpToolsForServer call hits the dedup guard (if (inProgressDiscovery) { await inProgressDiscovery; return; }) and waits on the hung promise forever.

Suggested change
reason: getErrorMessage(error),
} catch (error) {
this.serverDiscoveryPromises.delete(name);
recordStartupEvent(`mcp_server_ready:${name}`, {
outcome: 'failed',
reason: getErrorMessage(error),
});

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially real. Let me walk through the trace, because the fix is now load-bearing on the disconnect-on-timeout fix (your Critical thread above):

  1. runWithDiscoveryTimeout rejects on timeout. BEFORE my fix, the inner discoverMcpToolsForServer kept running — its connect() was still hung. The dedup Map entry stayed because the inner promise's finally (line 168-172) hadn't run.
  2. With the disconnect-on-timeout fix in 6b07a85, runWithDiscoveryTimeout now forces client.disconnect() when it times out. client.connect() then throws (transport closed mid-handshake), the inner promise rejects, and its finally clears the Map entry.

So the stale-entry symptom you described is no longer reachable in the steady state.

That said, I still added your suggested defensive this.serverDiscoveryPromises.delete(name) in the per-server catch (line 514) as belt-and-suspenders — if any future code path bypasses the disconnect (e.g. a custom client that ignores disconnect()), the Map entry won't leak.

// Remote transports (HTTP/SSE) carry network risk and get a shorter
// default; stdio servers we trust the user already runs locally.
const isRemote = !!(serverConfig?.httpUrl || serverConfig?.url);
return isRemote ? 5_000 : 30_000;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] discoveryTimeoutFor misses TCP/websocket transport

The remote transport detection !!(serverConfig?.httpUrl || serverConfig?.url) misses the tcp field (position 8 in MCPServerConfig, used for WebSocket transport). A TCP MCP server gets the 30s stdio default instead of the documented 5s remote default.

Suggested change
return isRemote ? 5_000 : 30_000;
const isRemote = !!(serverConfig?.httpUrl || serverConfig?.url || serverConfig?.tcp);

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right — fixed in 6b07a85. The remote check now includes tcp:

const isRemote = !!(
  serverConfig?.httpUrl ||
  serverConfig?.url ||
  serverConfig?.tcp
);

New test discoveryTimeoutFor treats websocket (tcp) transport as remote pins it by spying on setTimeout and asserting 5_000 is used (not 30_000).

Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
// setTools() emits `gemini_tools_updated` internally; we don't
// need to record anything from here. Errors are logged inside
// GeminiClient — never throw into the React tree.
void geminiClient.setTools().catch(() => {});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] setTools() silent error swallow with misleading comment

The comment says "Errors are logged inside GeminiClient" but GeminiClient.setTools() (client.ts:356-369) has no try/catch. If setTools() throws (e.g. from warmAll(), getFunctionDeclarations(), or getChat().setTools()), the error is silently discarded with no logging, making production debugging difficult.

Suggested change
void geminiClient.setTools().catch(() => {});
void geminiClient.setTools().catch((err) => {
debugLogger.error(
`setTools() batch-flush failed: ${err instanceof Error ? err.message : String(err)}`,
);
});

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6dcea68. The .catch(() => {}) is gone — setTools() errors are now routed through debugLogger.error:

return geminiClient.setTools().catch((err) => {
  debugLogger.error(
    `setTools() batch-flush failed: ${err instanceof Error ? err.message : String(err)}`,
  );
});

Same handler is reused by the new flushNow() path for the COMPLETED → finalize ordering fix (see r3223777822). Verified via direct read of client.ts:356-369 that GeminiClient.setTools() does not wrap warmAll() / getFunctionDeclarations() / getChat().setTools() — so this is the only debugging surface for those failures in production.

Comment thread packages/cli/src/gemini.tsx Outdated
// during initialize() / waitForMcpReady() are captured. Subsequent stdin
// reads / auth checks / prompt execution are not part of the
// "first-screen" budget.
finalizeStartupProfile(config.getSessionId());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Stream-json path finalizes startup profile before config.initialize()

For stream-json, config.initialize() is skipped (line 760-764) but await config.waitForMcpReady() (line 772, no-op) and finalizeStartupProfile() (line 777) still execute. The profile file is written without config_initialize_* checkpoints or any MCP events. When Session.initialize() later calls config.initialize() on the same config, the module-level finalized guard in startupProfiler.ts prevents event capture. Result: stream-json startup profiles are empty/misleading.

Suggested fix: move finalizeStartupProfile inside the if (inputFormat !== InputFormat.STREAM_JSON) block, or defer it to after Session.initialize() completes.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in a49b3e2. Moved finalizeStartupProfile(config.getSessionId()) inside the if (inputFormat !== InputFormat.STREAM_JSON) block (your first suggestion).

For stream-json, the profile is now finalized inside Session.ensureConfigInitialized AFTER config.initialize() + waitForMcpReady() complete:

// session.ts
await this.config.initialize(options);
await this.config.waitForMcpReady();
// ... mcp warning emit ...
finalizeStartupProfile(this.config.getSessionId());

This way the profile captures config_initialize_start, config_initialize_end, mcp_first_tool_registered, mcp_all_servers_settled, and gemini_tools_updated. The previous behavior produced empty stream-json profiles because the module-level finalized guard suppressed every event after the early finalize.

Comment thread packages/cli/src/gemini.tsx Outdated
// GeminiClient.setTools) into the cli's startup profiler. No-op when
// QWEN_CODE_PROFILE_STARTUP is unset because `recordStartupEvent` returns
// early in that case.
setStartupEventSink((name, attrs) => recordStartupEvent(name, attrs));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Profiler event sink always registered even when profiling is disabled

setStartupEventSink is called unconditionally. isStartupProfilerEnabled() exists in startupProfiler.ts:366 but is never imported or called. With QWEN_CODE_PROFILE_STARTUP unset, every recordStartupEvent() from core code traverses: sink null-check → arrow function → recordStartupEvent()enabled check → return — unnecessary overhead per event.

Suggested change
setStartupEventSink((name, attrs) => recordStartupEvent(name, attrs));
if (isStartupProfilerEnabled()) {
setStartupEventSink((name, attrs) => recordStartupEvent(name, attrs));
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in a49b3e2 with the exact gate you suggested:

if (isStartupProfilerEnabled()) {
  setStartupEventSink((name, attrs) => recordStartupEvent(name, attrs));
}

isStartupProfilerEnabled() is now imported alongside the other profiler helpers. Safe because initStartupProfiler() is called at the very top of packages/cli/index.ts (before this module is even imported), so the enabled flag is already set by the time main() reaches this guard.

For the QWEN_CODE_PROFILE_STARTUP=unset case the cost drops to a single null-check on the core side (if (sink) in startupEventSink.ts:45), no arrow allocation per event.

}
})
.catch((err: unknown) => {
this.debugLogger.error(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test comment - please ignore

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ CI is failing (Test windows-latest, Node 22.x). Review performed against commit 8aaec50.

This PR's core progressive-MCP design is solid, but there is 1 Critical issue and 2 Suggestions that need attention before merge.

}
})
.catch((err: unknown) => {
this.debugLogger.error(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Silent MCP discovery failure — no user-visible warning in non-interactive paths

startMcpDiscoveryInBackground()'s .catch() only calls debugLogger.error(). When ALL configured MCP servers fail (network partition, binary not on PATH, auth expiry), non-interactive paths (--prompt, stream-json, ACP) proceed with only built-in tools and zero user-visible output. The legacy synchronous path surfaced MCP failures visibly during config.initialize().

Impact: Silent regression — CI/script users get wrong behavior with no error message. Debug requires enabling debug logging.

Suggested change
this.debugLogger.error(
.catch((err: unknown) => {
const msg = `Background MCP discovery failed: ${err instanceof Error ? err.message : String(err)}`;
this.debugLogger.error(msg);
console.error(`Warning: ${msg}`);
});

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real silent regression — fixed in a49b3e2, but I took a slightly different approach than the in-line .catch you suggested. Here's why:

The outer .catch() on discoverAllMcpToolsIncremental(this) is unreachable in the all-servers-fail case. Per-server connect() / discover() failures are caught inside discoverAllMcpToolsIncremental's discoveryPromises.map(...) per-server try/catch (mcp-client-manager.ts ~line 511), which records mcp_server_ready:<name>:failed and continues. The outer promise resolves successfully even when every server failed.

Adding console.error to the outer .catch would never fire — confirmed by inspection of the per-server catch block.

Instead I added Config.getFailedMcpServerNames() which inspects getMCPServerStatus(name) !== CONNECTED for every non-disabled configured server, and the non-interactive entry points (gemini.tsx --prompt path, Session.ensureConfigInitialized for stream-json, acpAgent.runAcpAgent) emit a single user-visible stderr warning after waitForMcpReady() returns. Per-config-test coverage in config.test.ts.

Example output:

Warning: MCP server(s) failed to start: github, jira. Continuing with built-in tools and any servers that did connect. Re-run with QWEN_CODE_DEBUG=1 to see per-server reasons.


const onMcpUpdate = () => {
scheduleFlush();
if (manager.getDiscoveryState() === MCPDiscoveryState.COMPLETED) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] gemini_tools_lag metric non-deterministically missing

When mcp-client-update fires with COMPLETED, onMcpUpdate calls finalizeOnce() synchronously (line 585), setting finalized = true BEFORE the 16ms batch flush timer fires setTools(). recordStartupEvent('gemini_tools_updated') is dropped because it returns early when finalized is true. The gemini_tools_lag derived phase becomes undefined when no post-MCP event is captured. Whether it IS captured depends on per-server timing vs COMPLETED arrival.

Impact: The PR's advertised performance metric is unreliable in interactive mode.

Suggested fix: Defer finalizeOnce() until after the batch flush completes, or call setTools() + finalize atomically.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — fixed in 6dcea68. You're right that the previous onMcpUpdate had a finalize-before-flush ordering bug. New code:

const onMcpUpdate = () => {
  if (manager.getDiscoveryState() === MCPDiscoveryState.COMPLETED) {
    // Flush setTools() NOW (rather than the 16ms timer) and only
    // finalize after it runs — setTools() emits gemini_tools_updated,
    // and finalizing before it fires would drop that event because
    // the module-level `finalized` guard suppresses every subsequent
    // record. That dropped event is what gemini_tools_lag is derived
    // from.
    void flushNow().finally(finalizeOnce);
  } else {
    scheduleFlush();
  }
};

flushNow() returns a Promise so we can chain finalize via .finally. This guarantees gemini_tools_updated is recorded BEFORE finalized = true, so gemini_tools_lag will reliably appear in the derived phases.

Comment thread packages/cli/src/gemini.tsx Outdated
},
);
// Records the moment Ink has produced its first frame. AppContainer's mount
// effect runs after this — it carries the `config_initialize_*` and

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] first_paint checkpoint fires before actual Ink paint

profileCheckpoint('first_paint') is placed immediately after render() (line 332). Ink's render() returns synchronously but terminal output happens asynchronously through React reconciliation. The checkpoint fires before any pixels reach the terminal, making the to_first_paint derived phase artificially low.

Suggested fix: Rename to render_call_returned and update the comment, or hook into Ink's actual first-frame callback if available.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it's a semantic mismatch. Kept the checkpoint name first_paint for backward compatibility with previously-collected profile files (analysis tooling references the name), but the comment is now explicit:

// Records the moment Ink's `render()` call has returned, which is
// synchronous and happens before React reconciliation actually pushes
// bytes to the terminal. We intentionally keep the legacy name
// `first_paint` for backward compatibility with previously-collected
// profile files; the value is best read as "render call returned"
// rather than literal pixel paint.

If we want a true first-frame hook, Ink's useApp().rerender callback or stdout drain detection would work, but neither is exposed today and the value of first_paint is already useful as an upper bound for "time to render call returned". Open to revisiting in a follow-up if the gap matters in practice.

秦奇 and others added 3 commits May 12, 2026 14:27
Addresses review feedback on PR #3994:

- Skip user-disabled servers in discoverAllMcpToolsIncremental. The new
  incremental path used to iterate Object.entries(servers) without
  consulting isMcpServerDisabled, so a server the user had explicitly
  turned off would still get connected and its tools registered.
  Mirrors the existing protection in discoverAllMcpTools.

- Disconnect the underlying client when runWithDiscoveryTimeout fires.
  Without this, the inner discoverMcpToolsForServer kept running after
  the timeout rejected the outer promise — if discover() eventually
  succeeded it would register the late server's tools into the live
  toolRegistry (a silent registration vector, especially exploitable
  with a 0/negative discoveryTimeoutMs override).

- Clamp discoveryTimeoutMs to [100ms, 300_000ms]. 0/negative/Infinity
  values previously passed through to setTimeout unvalidated and made
  the silent-registration bug above trivially reachable.

- Classify the `tcp` (WebSocket) transport field as remote so hung WS
  handshakes use the 5s default instead of the 30s stdio default.

- Defensive delete of serverDiscoveryPromises[name] in the per-server
  catch so a doomed/orphan entry can't briefly short-circuit a
  subsequent discoverMcpToolsForServer call.

Adds focused tests for each fix.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
… visibility

Addresses review feedback on PR #3994:

- Restore writeRuntimeStatus + markRuntimeStatusEnabled in
  startInteractiveUI. The progressive-MCP diff inadvertently dropped
  the runtime.json sidecar write from the interactive entry point,
  leaving Config.refreshSessionId()'s session-swap refresh as dead
  code and silently breaking external integrations (terminal
  multiplexers, IDE integrations, status daemons) that map PID →
  sessionId via runtime.json.

- Add Config.getFailedMcpServerNames() and surface a stderr warning
  in --prompt / stream-json / ACP entry points when one or more MCP
  servers failed during background discovery. Per-server errors are
  caught inside discoverAllMcpToolsIncremental and never reached a
  TTY otherwise, so a script using non-interactive mode with broken
  MCP config would silently run with only built-in tools — a
  regression vs the legacy synchronous path.

- Pass the parsed `settings` object through to
  runNonInteractiveStreamJson. The new call site dropped the
  argument, falling back to createMinimalSettings() and losing any
  user-configured permission / approval / hook setup for stream-json
  sessions. Added regression assertion to gemini.test.tsx.

- Move finalizeStartupProfile out of gemini.tsx's stream-json branch
  and into Session.ensureConfigInitialized so it runs AFTER
  config.initialize() / waitForMcpReady() in stream-json. Previously
  the profile was finalized before any MCP / config_initialize_*
  events were emitted, producing empty stream-json profiles.

- Gate setStartupEventSink registration on isStartupProfilerEnabled()
  so core-side recordStartupEvent calls short-circuit at the first
  null-check when profiling is disabled, instead of going through an
  arrow wrapper and the profiler's own enabled gate.

- Tighten the type-unsafe ToolRegistry cast in
  startMcpDiscoveryInBackground to preserve the typed return signature
  so a rename of getMcpClientManager would be flagged at this call
  site (kept the optional-chain guard for tests that stub
  ToolRegistry as a plain object).

- Re-document first_paint as "render call returned" so consumers don't
  confuse Ink's synchronous render() return with literal pixel paint.
  Kept the checkpoint name for backward compatibility with collected
  profiles.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…AppContainer

Addresses review feedback on PR #3994:

- Restore the terminal-resize useEffect that calls
  repaintStaticViewport() when terminalWidth changes. The progressive-
  MCP diff removed previousTerminalWidthRef + the repaint useCallback
  + the resize useEffect, so tmux pane resizes and fullscreen toggles
  leave the static region rendered at the old width — header content
  visibly tears until something else triggers refreshStatic.

- Pin the gemini_tools_lag startup metric. The previous onMcpUpdate
  handler called finalizeOnce() synchronously when discovery reached
  COMPLETED, but the pending setTools() batch was still 16ms away.
  setTools() emits `gemini_tools_updated` — when finalize ran first
  the profile's `finalized` guard suppressed that event, so
  gemini_tools_lag came out undefined in interactive mode. New
  onMcpUpdate flushes setTools() NOW on COMPLETED and only finalizes
  after the flush resolves, guaranteeing the event lands.

- Log setTools() batch-flush errors via debugLogger instead of
  silently swallowing them. GeminiClient.setTools() has no try/catch
  around warmAll() / getFunctionDeclarations() / getChat().setTools();
  the previous `.catch(() => {})` would have hidden production
  tool-registration regressions completely.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@chiga0

chiga0 commented May 12, 2026

Copy link
Copy Markdown
Collaborator Author

@wenshao — addressed all four rounds of review feedback. Summary of dispositions across the 16 substantive inline comments (skipped the explicit Test comment - please ignore):

Fixes landed

[Critical] All 5 accepted, fixed in 3 commits. All confirmed by direct code-path trace.

  • 6b07a85 fix(core): harden progressive MCP discovery against silent regressions

    • r3220395459 silent tool registration after timeout → disconnect on timeout + timedOut flag suppresses late success
    • r3223481404 disabled MCP servers skipped → isMcpServerDisabled check added
    • r3223481411 stale serverDiscoveryPromises entry → defensive delete + becomes moot after disconnect-on-timeout, kept as belt-and-suspenders
    • r3220395471 discoveryTimeoutMs validation → clamped [100ms, 300_000ms] + Number.isFinite
    • r3223481420 discoveryTimeoutFor missed tcp (WebSocket) → added
    • 4 new tests pin each.
  • a49b3e2 fix(cli): restore runtime.json sidecar and harden non-interactive MCP visibility

    • r3220395416 runtime.json sidecar deletion → restored both writeRuntimeStatus and markRuntimeStatusEnabled (which arms the session-swap refresh)
    • r3223777811 silent MCP failure → added Config.getFailedMcpServerNames() + stderr warning at all 3 non-interactive entry points (--prompt, stream-json, ACP). The suggested .catch(console.error) was unreachable because per-server failures are swallowed inside discoverAllMcpToolsIncremental; this approach actually surfaces the regression.
    • r3220395520 stream-json settings arg drop → restored + regression assertion in gemini.test.tsx
    • r3223481427 stream-json finalize-before-init → moved finalize into Session.ensureConfigInitialized after waitForMcpReady
    • r3223481429 profiler sink always registered → gated on isStartupProfilerEnabled()
    • r3220395484 type-unsafe ToolRegistry cast → tightened to preserve typed return so renames flag
    • r3223777825 first_paint timing → documented as "render call returned", kept the name for profile compatibility
  • 6dcea68 fix(cli): restore resize repaint and pin gemini_tools_lag capture in AppContainer

    • r3220395499 resize handler deletion → restored previousTerminalWidthRef, repaintStaticViewport, and the resize useEffect
    • r3223777822 gemini_tools_lag non-deterministically missing → onMcpUpdate now flushes setTools() before finalize on COMPLETED via flushNow().finally(finalizeOnce)
    • r3223481423 setTools silent error swallow → routed through debugLogger.error

Deferred with reasoning

  • r3220395511 batch-flush useEffect untested → won't add the dedicated useEffect tests in this PR pass (timer + event emitter harness is non-trivial and prone to flakes), but lower-layer contracts (mcp-client-manager.test.ts) now pin each dependent invariant: disabled-skip, COMPLETED trailing emit, disconnect-on-timeout, timeoutMs clamp, tcp classification. Will track a follow-up issue.

CI

Test (windows-latest, Node 22.x) failure on InputPrompt.test.tsx → accepts and submits the prompt suggestion on Enter when the buffer is empty — this PR does not touch InputPrompt.tsx or its test file (confirmed via git diff main..HEAD -- packages/cli/src/ui/components/InputPrompt*). Same test has flaked on main's recent CI runs as well. Unrelated to this PR's changes; will re-run.

Verified locally: typecheck clean on both packages; affected vitest suites (mcp-client-manager 16, config 140, gemini 14, session 27, acpAgent 34, AppContainer 61, startupProfiler 19, nonInteractiveCli 42) all pass.

Ready for re-review when you have a moment.

@chiga0
chiga0 requested a review from wenshao May 12, 2026 07:13

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional finding not mappable to a single diff line:

[Critical] McpClient.discover() in packages/core/src/tools/mcp-client.ts:169 lacks error status updateMcpClient.connect() calls this.updateStatus(MCPServerStatus.DISCONNECTED) on error, but McpClient.discover() never calls updateStatus() on success or failure. If discoverTools or discoverPrompts throw (e.g., server crashes mid-discovery), the status stays CONNECTED. Since Config.getFailedMcpServerNames() (new in this PR) checks status !== CONNECTED, such servers won't appear in the failure list — silent data loss. Fix: wrap the body of discover() in try/catch and call this.updateStatus(MCPServerStatus.DISCONNECTED) on error.

Also see the 3 inline comments below for the other findings.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

serverName: name,
});
}
recordStartupEvent(`mcp_server_ready:${name}`, { outcome: 'ready' });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] mcp_server_ready incorrectly reports outcome: 'ready' for failed connections

discoverMcpToolsForServerInternal catches all errors from connect()/discover() without re-throwing, so the try block in discoverAllMcpToolsIncremental always succeeds — unconditionally recording mcp_first_tool_registered and outcome: 'ready' events. Only runWithDiscoveryTimeout rejections reach the catch block. Any non-timeout failure (auth error, server crash, missing tools) is incorrectly recorded as success in the startup profile.

Suggested change
recordStartupEvent(`mcp_server_ready:${name}`, { outcome: 'ready' });
const client = this.clients.get(name);
const actuallyReady =
client &&
getMCPServerStatus(name) === MCPServerStatus.CONNECTED;
if (actuallyReady) {
if (!firstToolEventFired) {
firstToolEventFired = true;
recordStartupEvent('mcp_first_tool_registered', {
serverName: name,
});
}
recordStartupEvent(`mcp_server_ready:${name}`, {
outcome: 'ready',
});
} else {
recordStartupEvent(`mcp_server_ready:${name}`, {
outcome: 'failed',
reason: 'connection or discovery error',
});
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

@chiga0 chiga0 May 12, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0996ee8. You're right — discoverMcpToolsForServerInternal swallows connect/discover errors without re-throwing (best-effort discovery), so the try block here resolved for every failure except the timeout path. The fix consults the actual server status (now correctly DISCONNECTED after the McpClient.discover fix above) before recording outcome: 'ready', and emits outcome: 'failed' otherwise. The mcp_first_tool_registered event is gated on the same check so a failed server can't pollute that user-facing metric either. Test added: discoverAllMcpToolsIncremental records 'failed' outcome for swallowed connect errors.

// see it reconnected by the incremental path. Without this, the
// PR-A background path silently re-registers tools the user has
// told us to ignore.
if (cliConfig.isMcpServerDisabled(name)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] discoverAllMcpToolsIncremental fails to clean up enabled→disabled server transitions

When a server is disabled, the loop skips it with continue but does not remove its existing client (this.clients), registered tools (ToolRegistry), or health checks. The removeServer() method should be called for newly-disabled servers to ensure state consistency. Same issue at line 102 in connectAndDiscover. The synchronous discoverAllMcpTools handles this correctly by calling this.stop() first.

Suggested change
if (cliConfig.isMcpServerDisabled(name)) {
if (cliConfig.isMcpServerDisabled(name)) {
debugLogger.debug(`Skipping disabled MCP server: ${name}`);
if (this.clients.has(name)) {
this.removeServer(name);
}
continue;
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0996ee8. Agreed — the previous code just continued past disabled servers, so a mid-session enabled→disabled transition (e.g. via /mcp disable foo or by editing settings) left the existing client, registered tools, health check, and global status entry in place. The disabled branch now calls removeServer(name) for any already-known client, which disconnects, drops the client entry, removes the server's tools from the registry, stops the health check, and removes the global status. Test added: discoverAllMcpToolsIncremental tears down enabled→disabled transitions. Note: I did NOT mirror this into connectAndDiscover at line ~102 — that's the legacy synchronous discoverAllMcpTools path, which already calls this.stop() at the top (line 89) and so already starts from a clean slate. The bug was specific to the incremental path.

Comment thread packages/core/src/config/config.ts Outdated
*/
private startMcpDiscoveryInBackground(): void {
// `getMcpClientManager` is a public method on `ToolRegistry` (added
// alongside this PR), so we call it directly — no defensive cast.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Comment claims "no defensive cast" but code still uses a type assertion with optional chaining

The comment says getMcpClientManager is called directly without a defensive cast, but the code still uses as ToolRegistry & { getMcpClientManager?: ... } with ?.(). Since getMcpClientManager is a public method on ToolRegistry, the cast is technically unnecessary — it exists only for test compatibility where ToolRegistry is stubbed as a plain object. Update the comment to explain the real reason.

Suggested change
// alongside this PR), so we call it directly — no defensive cast.
// Tests stub `toolRegistry` as a plain object, so we guard with
// optional chaining to avoid crashing the production path.
const manager = (
this.toolRegistry as ToolRegistry & {
getMcpClientManager?: () => ReturnType<ToolRegistry['getMcpClientManager']>;
}
).getMcpClientManager?.();

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d20bbf9 (comment-only). You're right that the existing wording contradicted the code — the cast remains, just for a different reason than the cast it replaced in a49b3e2. The new comment makes the test-stub motivation explicit and also notes that the inner ReturnType<ToolRegistry['getMcpClientManager']> shape means a future rename of getMcpClientManager on ToolRegistry still surfaces here as a type error (rather than silently falling through to if (!manager) return like the previous hand-rolled { discoverAllMcpToolsIncremental: ... } shape would have). I went slightly more verbose than your suggested wording to retain that point — happy to trim if you prefer the shorter version.

秦奇 and others added 2 commits May 12, 2026 19:23
Addresses three review findings on PR #3994:

- McpClient.discover() now flips the client status to DISCONNECTED before
  re-throwing. Previously, a server that connected successfully but whose
  discoverPrompts / discoverTools then rejected (or that returned no
  prompts and no tools) would remain CONNECTED in the global status
  registry. Config.getFailedMcpServerNames() filters by
  `status !== CONNECTED`, so such servers were silently omitted from the
  non-interactive failure banner and the Footer's MCP health pill kept
  counting them as healthy.

- discoverAllMcpToolsIncremental no longer records `outcome: 'ready'`
  for servers whose connect/discover threw. The inner
  discoverMcpToolsForServerInternal catches errors without re-throwing
  (best-effort discovery semantics), so the try block resolved even for
  failures — only the runWithDiscoveryTimeout path reached the catch.
  Auth errors, server crashes, and missing-tools responses were therefore
  recorded as success in the startup profile. We now consult the actual
  server status (now correctly DISCONNECTED after the first fix) before
  emitting `ready`, and emit `outcome: 'failed'` otherwise.
  `mcp_first_tool_registered` is gated on the same check so a failed
  server can't pollute that user-facing metric.

- discoverAllMcpToolsIncremental tears down enabled→disabled mid-session
  transitions. When a previously-connected server is disabled (e.g. via
  `/mcp disable foo` or by editing settings), the incremental path used
  to just `continue` past it, leaving its client, tools, health check,
  and global status entry in place. Now calls removeServer() for any
  already-known client we encounter in the disabled branch.

Adds focused tests for each fix.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…ackground

Addresses review feedback on PR #3994. The previous comment claimed the
call site uses "no defensive cast" but the code still casts via
`as ToolRegistry & { getMcpClientManager?: ... }`. Reword to explain
the cast's actual purpose: it exists only because some tests stub
ToolRegistry as a plain object, so we use optional chaining to avoid
crashing the init path when those tests run. Also note that the inner
shape now uses `ReturnType<ToolRegistry['getMcpClientManager']>` — a
future rename of the production method still surfaces as a type error
at this call site rather than silently falling through to the
`if (!manager)` branch.

Comment-only change; no behavior diff.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@chiga0

chiga0 commented May 12, 2026

Copy link
Copy Markdown
Collaborator Author

Re: top-level [Critical] on McpClient.discover() lacking error status update — fixed in 0996ee8. You're right: McpClient.connect() had a try/catch that flipped to DISCONNECTED on error, but McpClient.discover() did not, so a server that connected but whose discoverPrompts/discoverTools then threw — or whose tools/list returned empty — stayed CONNECTED in the global registry. Config.getFailedMcpServerNames() filters by status !== CONNECTED, so such servers were silently omitted from the non-interactive failure banner (and the Footer's MCP health pill kept counting them as healthy).

The fix wraps the body of discover() in try/catch, calls this.updateStatus(MCPServerStatus.DISCONNECTED) on error, then re-throws. The caller (McpClientManager.discoverMcpToolsForServerInternal) still catches and logs as before — we just need the global status registry to reflect reality.

Test added in mcp-client.test.ts: flips status to DISCONNECTED when discover() throws. This fix is also what makes the comment-3225568974 outcome: 'failed' check work: the manager consults getMCPServerStatus(name) to decide which outcome to record, and that value now correctly reflects discover failures.

@chiga0

chiga0 commented May 12, 2026

Copy link
Copy Markdown
Collaborator Author

All round-5 feedback addressed:

  • 0996ee8: mcp_server_ready outcome now gated on getMCPServerStatus === CONNECTED (was incorrectly reporting 'ready' for failed connections); McpClient.discover() wrapped in try/catch with DISCONNECTED status update on error; discoverAllMcpToolsIncremental cleans up enabled→disabled server transitions via removeServer().
  • d20bbf9: clarified the ToolRegistry cast comment in startMcpDiscoveryInBackground (test-stub compatibility + rename-safe via ReturnType).

These 4 items are NEW findings against the post-round-4 codebase (no overlap with prior fixes in 6b07a85 / a49b3e2 / 6dcea68).

mcp-client / mcp-client-manager / config tests all green (35+18+140). Ready for re-review.

@chiga0
chiga0 requested a review from wenshao May 12, 2026 12:35

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ CI is failing (Test windows-latest, Node 22.x).

[Suggestion] Duplicate MCP failure warning logic across 3 call sitespackages/cli/src/acp-integration/acpAgent.ts, packages/cli/src/gemini.tsx, packages/cli/src/nonInteractive/session.ts. The same getFailedMcpServerNames() + stderr.write pattern is repeated with near-identical logic. Extract into a shared helper on Config or as an importable utility to avoid future drift in message format, throttling, or log-target changes.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

// ever reach the registry.
const client = this.clients.get(serverName);
if (client) {
void client.disconnect().catch((err) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] TOCTOU race: fire-and-forget disconnect() allows tools to register after timeout fires

The timeout handler calls void client.disconnect().catch(...) without await, then immediately reject(). Since disconnect() is async (awaits transport.close()), there is a window where connect() has already completed and discover() is mid-flight registering tools — those tools land in the registry after timeout. The code comment already labels this as a "remote-exploitable silent-tool-registration vector" but the fix is incomplete.

Suggested change
void client.disconnect().catch((err) => {
await client.disconnect();
reject(new TimeoutError(`MCP server '${serverName}' discovery timed out after ${timeoutMs}ms`));

or, after disconnect completes, remove the server's tools from toolRegistry.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b6e97d9. Switched the timeout handler to await client.disconnect() so transport.close() lands before reject, and added this.toolRegistry.removeMcpToolsByServer(serverName) afterwards to drop any tools that did slip into the registry during the disconnect window (your second suggestion). Together they close the silent-tool-registration race: even if discover() synchronously pumps tools/list into the registry just before the transport closes, the cleanup call removes them.

this.updateStatus(MCPServerStatus.DISCONNECTED);
throw error;
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] isDisconnecting flag prevents updateStatus(DISCONNECTED) from reaching the global registry for timeout-ed servers

When the timeout callback in runWithDiscoveryTimeout calls client.disconnect(), McpClient.disconnect() sets this.isDisconnecting = true (line 204) before calling this.updateStatus(DISCONNECTED) (line 209). However, updateStatus() has a guard at line 246: if (this.isDisconnecting) { return; } that skips updateMCPServerStatus() — so the global status registry never receives the DISCONNECTED update.

This affects the discover() catch block at this line too: after disconnect() tears down the transport, the in-flight connect()/discover() throws, and this.updateStatus(DISCONNECTED) fires here — but the global update is silently swallowed. The server stays CONNECTED in the global registry forever. Config.getFailedMcpServerNames() — which filters on status !== CONNECTED — never reports it.

Suggested change
}
// In McpClient.disconnect(), move updateStatus before isDisconnecting:
this.updateStatus(MCPServerStatus.DISCONNECTED);
this.isDisconnecting = true;

or, in the timeout callback, call updateMCPServerStatus(name, DISCONNECTED) directly on the global registry before client.disconnect().

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b6e97d9. McpClient.disconnect() now writes MCPServerStatus.DISCONNECTED to the global registry directly (via updateMCPServerStatus) BEFORE setting isDisconnecting = true, so the intentional disconnect notification can't be swallowed by the guard whose only purpose is to block stale connect() catch updates. The local this.status is also flipped first so a concurrent discover() reading it doesn't proceed to register tools. The round-5 getMCPServerStatus === CONNECTED gate now correctly distinguishes ready vs failed because timeout-disconnected servers actually reach DISCONNECTED in the registry. Added a regression test in mcp-client.test.ts.

if (prompts.length === 0 && tools.length === 0) {
throw new Error('No prompts or tools found on the server.');
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Healthy MCP servers with 0 tools/prompts are reported as "failed"

McpClient.discover() throws "No prompts or tools found on the server." when both prompts and tools arrays are empty. The surrounding try/catch at this line calls updateStatus(DISCONNECTED), causing getFailedMcpServerNames() to include the server in the "Warning: MCP server(s) failed to start" banner. A server that connects successfully but exposes no tools (e.g., a pure resource server) is healthy — consider keeping its status as CONNECTED with 0 tools.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining for this PR. The if (prompts.length === 0 && tools.length === 0) throw check predates this PR — git log on packages/core/src/tools/mcp-client.ts shows it was introduced as part of an earlier refactor and we only re-arranged the surrounding try/catch in round 5. Loosening it to treat resource-only servers as healthy would change the getFailedMcpServerNames() semantics that the Footer health pill and non-interactive failure banner depend on; that belongs in a focused follow-up rather than this progressive-discovery PR. Happy to take that on as a separate change if you file it.

`Continuing with built-in tools and any servers that did connect.\n`,
);
}
// Finalize the startup profile here so `config_initialize_*` and the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Stream-json path is missing config_initialize_start / config_initialize_end profiler checkpoints

Session.ensureConfigInitialized() calls config.initialize() directly without wrapping it in profileCheckpoint('config_initialize_start') / profileCheckpoint('config_initialize_end'), unlike the interactive and non-stream-json paths in gemini.tsx. The comment here acknowledges the timing difference but doesn't address the missing config_initialize_dur derived phase in stream-json session profiles. Consider adding the checkpoints (gated on isStartupProfilerEnabled()) for consistent profiling across all modes.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b6e97d9. Added profileCheckpoint('config_initialize_start') / _end around this.config.initialize(options) in Session.ensureConfigInitialized(), mirroring the non-stream-json branch in gemini.tsx so the config_initialize_dur derived phase shows up consistently across all modes. profileCheckpoint is a no-op when QWEN_CODE_PROFILE_STARTUP is unset so this adds zero overhead off the profiling path.

@wenshao

wenshao commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Overview

This PR replaces synchronous MCP discovery during Config.initialize() with a fire-and-forget background path so the interactive cli no longer blocks on slow / hung MCP servers. The numbers are real (7.1 s → 0.47 s TTI with 2 fast + 1 slow MCP), the non-interactive escape hatches are wired correctly, and the change is well-tested (+5 tests, 318 total). The PR is well-scoped and ships with a documented rollback flag (QWEN_CODE_LEGACY_MCP_BLOCKING=1).

Strengths

  • Defensible perf win backed by Welch t-tests + reproducible profiler harness. The derivedPhases.to_input_enabled metric is the right TTI proxy.
  • Behavioral audit of every non-interactive path is rigorous. --prompt, stream-json, and ACP all await waitForMcpReady() before the first model send — no silent regression for CI/scripts/IDE integrations.
  • runWithDiscoveryTimeout + force-disconnect correctly addresses a real silent-tool-registration vector. The "actual server status" cross-check rather than trusting the inner promise's resolved state is the right call.
  • MCPServerStatus cross-check in mcp_server_ready event prevents the auth-failure-as-success profile pollution.
  • StartupEventSink is the right structure for core → cli decoupling, and the no-op path is genuinely O(1).
  • McpClient.discover() now flips status to DISCONNECTED on throw — solid hygiene that getFailedMcpServerNames() relies on. The test in mcp-client.test.ts covers it well.
  • Trailing mcp-client-update after COMPLETED — without that the deferred-finalize subscriber would never observe the terminal state; well-targeted test coverage.

Issues / suggestions

Blocking-ish

  • AppContainer.tsx: constants wedged between imports. MCP_BATCH_FLUSH_MS and STARTUP_PROFILE_FINALIZE_CAP_MS are declared after some imports and before import { useHistory } from './hooks/useHistoryManager.js';. This violates import/first style and likely the project's ESLint config — confirm npm run lint actually passes on this file, or move the constants below all imports.

  • Untracked re-entrant MCP discovery for waitForMcpReady. mcpDiscoveryPromise is set exactly once inside startMcpDiscoveryInBackground. A subsequent re-discovery (e.g. /mcp reload, OAuth refresh, an extension calling discoverAllMcpToolsIncremental) does NOT update it, so waitForMcpReady() would return stale state for any non-interactive caller invoked after a runtime reload. For PR-A's startup scope this is fine, but document the limitation in the JSDoc on waitForMcpReady and/or guard startMcpDiscoveryInBackground to chain instead of replace.

Non-blocking

  • Double setTools() on the interactive path. startMcpDiscoveryInBackground calls geminiClient.setTools() in its .then(), AND AppContainer's batch-flush effect calls setTools() on mcp-client-update. Idempotent, but it's a duplicated responsibility — pick one owner. The cleanest split is "core owns non-interactive setTools, AppContainer owns interactive" guarded by config.isInteractive().

  • ToolRegistry & { getMcpClientManager?: () => ... } cast in startMcpDiscoveryInBackground. The comment justifies it for stub objects in tests, but production code shouldn't carry this overhead. Cleaner: fix the test mocks to implement the real public interface, then drop the cast and the if (!manager) return defense. (You'd also stop suppressing real future regressions where a refactor accidentally returns undefined.)

  • startMcpDiscoveryInBackground .then() calls setTools() unconditionally. Even when zero servers settled successfully (all failed). Cheap, but a failedMcpServers.length < totalServers guard would skip the no-op.

  • Defensive this.serverDiscoveryPromises.delete(name) in discoverAllMcpToolsIncremental's catch. The justification says "can reject before that finally runs" — but runWithDiscoveryTimeout rejecting doesn't prevent the inner discoverMcpToolsForServer's finally from clearing the entry once the disconnected handshake errors out. The window is real but very short; if you keep the delete, the comment should describe the actual race (concurrent re-entry within the inner promise's still-pending lifetime) rather than implying the finally never runs.

  • STARTUP_PROFILE_FINALIZE_CAP_MS = 35_000 is hardcoded against the 30 s stdio default. A user-overridden discoveryTimeoutMs: 60_000 would have its mcp_all_servers_settled event dropped because the profile finalizes at 35 s. Derive from max(discoveryTimeoutMs across all servers) + buffer if you want this fully correct, or document.

  • MCPServerConfig.discoveryTimeoutMs as a positional ctor param. The constructor is already at ~25 positional parameters; adding more positional fields here is fragile. Pre-existing wart, but consider this PR's introduction as a forcing function to migrate to an options object — a single struct-style refactor of MCPServerConfig is much cheaper now than after another five positional adds.

  • gemini.tsx, session.ts, and acpAgent.ts duplicate the failed-MCP warning emission. Three near-identical blocks that compose the same Warning: MCP server(s) failed to start: … message and check typeof config.getFailedMcpServerNames === 'function'. Extract reportFailedMcpServers(config, write) into a shared helper — easier to keep the wording consistent and the defensive-typeof check would live in one place.

  • discoverAllMcpToolsIncremental disabled-server teardown is sequential. for (const [name] of …) + await this.removeServer(name) serializes disconnect across servers. Fine for normal config sizes; could matter for tenants with many SDK servers. Switch to collecting names then Promise.all(removeServer) if you want a follow-up.

  • MCP_BATCH_FLUSH_MS = 16 comment claims "validated by Claude's production deployment." Cite an internal source or drop the claim — public reviewers can't verify it, and tying behavior to an unverifiable external implementation creates a maintenance liability.

  • AppContainer's 100-line useEffect. finalizeOnce / flushNow / scheduleFlush / onMcpUpdate plus the legacy/no-MCP branch — extract to a useMcpProgressiveSync(config, isConfigInitialized) hook. Easier to unit-test in isolation, easier to read in AppContainer.tsx.

  • Unrelated scope creep. A few changes don't belong in a progressive-MCP PR and should ideally be their own commits:

    • previousTerminalWidthRef declaration relocation + new comment.
    • The gemini.test.tsx settingsArg regression guard (this is a fix for a different bug on the same branch — call it out in the PR description or split it).
    • The runtimeStatusEnabled comment edits in gemini.tsx.
  • getMCPServerStatus is module-level global state. Pre-existing, but Config.getFailedMcpServerNames() consults this global rather than the manager's clients map — meaning two concurrent Config instances (e.g. multi-session ACP) would see each other's status. ACP's per-session config is created inside QwenAgent.createSessionConfig and awaits its own discovery; check whether the global registry race is benign there before merging.

Test coverage

Solid. Highlights:

  • The clamp test is a bit fragile against vitest's own setTimeout calls but adequately defended by the "100 AND 300000 by coincidence" reasoning.
  • The runWithDiscoveryTimeout disconnect-on-timeout test directly exercises the silent-tool-registration vector — good.
  • The failed-outcome test closes the gap between connect errors and profile event outcomes.

Worth adding:

  • A test for the interactive double-setTools() deduplication if you keep both call sites (or just verify the count via the event emit count).
  • A test for waitForMcpReady after a re-discovery: invoke discoverAllMcpToolsIncremental a second time and confirm whether waitForMcpReady should block or not — pick the semantics and lock them in.

Security

  • Disconnect-on-timeout closes the silent-registration vector from a slow / attacker-controlled remote server. Good.
  • discoveryTimeoutMs clamp (100 ms floor, 5 min ceiling) prevents the 0 / negative bypass.
  • getFailedMcpServerNames skips user-disabled servers — won't spuriously warn after the user explicitly turned a server off.

No new attack surface I can see. The QWEN_CODE_LEGACY_MCP_BLOCKING env var is a developer escape hatch, not a security toggle.

Risk assessment

  • Behavioral risk: low. Non-interactive paths await waitForMcpReady — same observable tool surface as today. The single risk vector is an extension/plugin that reads config.getToolRegistry() synchronously after config.initialize() resolves and assumes MCP tools are present. PR description flags this; reviewers should grep extension callers before merging.
  • Performance risk: low. Profiler is zero-cost when disabled (verified by the -1.12% Welch test). Heap snapshot opt-out exists.
  • Rollback: clean. QWEN_CODE_LEGACY_MCP_BLOCKING=1 restores prior behavior. Single-commit revert otherwise.

Recommendation

Approve with the import ordering and waitForMcpReady re-discovery docs as required changes; the rest as follow-ups. The split out of unrelated scope (terminal width ref, settings regression guard) would also help the merge story but isn't blocking.

Addresses two critical findings on PR #3994 round 6:

- runWithDiscoveryTimeout no longer uses fire-and-forget disconnect. The
  prior `void client.disconnect()` returned before `transport.close()`
  landed, leaving a window where an in-flight `discover()` could pump
  `tools/list` through the transport and synchronously register tools
  into the live registry BEFORE the close took effect. The earlier fix
  comment described this as a "remote-exploitable silent-tool-registration
  vector"; the await closes the timing window but doesn't help if tools
  already landed, so we also drop them with `removeMcpToolsByServer()`
  after the disconnect resolves. No-op when discover hadn't reached
  registration yet.

- McpClient.disconnect() now writes DISCONNECTED to the global registry
  directly. Previously, `isDisconnecting = true` was set BEFORE the
  internal `updateStatus(DISCONNECTED)` call, and `updateStatus`'s guard
  (designed to suppress LATE writes from a stale `connect()` catch)
  silently swallowed the write. The global stayed CONNECTED forever for
  timeout-disconnected servers, so `Config.getFailedMcpServerNames()`
  (which filters `status !== CONNECTED`) omitted them from the
  non-interactive failure banner and the Footer's MCP health pill kept
  counting them as healthy. This invalidated the round-5
  `getMCPServerStatus === CONNECTED` gate, which would always pass the
  "ready" check for timed-out servers. The guard stays in place for its
  original purpose; the legitimate disconnect→DISCONNECTED notification
  now bypasses it by writing the registry directly.

Also adds the `config_initialize_start` / `_end` profiler checkpoints
to `Session.ensureConfigInitialized()` so stream-json startup profiles
include the same derived `config_initialize_dur` phase as the
non-stream-json branch in gemini.tsx (round 6 [Suggestion]).

Tests cover (a) the disconnect-and-cleanup path on timeout and (b) the
intentional-disconnect global registry propagation regression.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@chiga0

chiga0 commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author

Round 6 follow-ups in b6e97d9:

  • [Critical] TOCTOU race in runWithDiscoveryTimeout — fixed. Timeout handler now awaits client.disconnect() so transport.close() lands before reject, and follows up with toolRegistry.removeMcpToolsByServer(name) to drop any tools that registered during the race window. The earlier comment's "silent-tool-registration vector" wording is now actually addressed end-to-end.
  • [Critical] isDisconnecting swallows the DISCONNECTED registry update — fixed. McpClient.disconnect() now writes DISCONNECTED to the global registry directly (bypassing the updateStatus guard whose only purpose is to suppress stale connect() catch updates). The round-5 getMCPServerStatus === CONNECTED gate now distinguishes ready vs failed correctly.
  • [Suggestion] 0-tools/0-prompts "failed" classification — declined as out-of-scope (predates this PR; happy to take it on as a focused follow-up).
  • [Suggestion] Stream-json missing config_initialize_* checkpoints — fixed by wrapping config.initialize() in Session.ensureConfigInitialized() with the same profileCheckpoint pair the non-stream-json branch uses.
  • [Suggestion] Duplicate MCP failure warning helper — acknowledged; deferring to a focused refactor PR. Extracting a shared logFailedMcpServers(config) helper at this point would touch gemini.tsx, session.ts, and acpAgent.ts non-trivially and isn't on the critical path for the progressive-MCP work this PR ships.

Tests cover both critical paths (mcp-client-manager.test.ts for the disconnect+removeMcpToolsByServer flow; mcp-client.test.ts for the intentional-disconnect global registry propagation).

Windows CI failure: the failing tests (crawler.test.ts and gitDiff.test.ts) live in packages/core/src/utils/ which this PR does not touch. They were introduced by PR #3214 (replaced fdir with git ls-files + ripgrep fallback) and the failures look like Windows rmdir EBUSY flakes on the synthetic merge ref. Main's own most-recent Windows CI run (25772181577 / commit 7099165) passed these tests cleanly, so this is unrelated to the PR. Re-running CI on the next push or rebasing on main should clear it.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] AppContainer MCP batch-flush useEffect untested (packages/cli/src/ui/AppContainer.tsx:526-628)

The ~100-line useEffect that implements 16ms batch-flushed setTools() + deferred profile finalize + mcp-client-update subscription — the core wiring for interactive progressive MCP availability — has no component-level tests. AppContainer.test.tsx has zero matches for MCP_BATCH, mcp-client-update, onMcpUpdate, or flushNow.

Suggested fix: Add AppContainer.test.tsx cases covering: batch debouncing, COMPLETED→immediate flush+finalize, 35s cap fallback, cleanup removes listeners/timers.


Test coverage gaps (Suggestion):

  • config.ts:1297-1344startMcpDiscoveryInBackground setTools() call and error propagation
  • session.ts:146-180ensureConfigInitialized waitForMcpReady, getFailedMcpServerNames, profile checkpoints
  • gemini.tsx:396-401setStartupEventSink bridge gating logic
  • config.ts:1400-1414getFailedMcpServerNames DISCONNECTED/CONNECTING/mixed states
  • mcp-client-manager.ts:658-674discoveryTimeoutFor httpUrl/url/non-finite edge cases
  • mcp-client-manager.ts:585-637runWithDiscoveryTimeout non-timeout success/failure paths
  • client.ts:366gemini_tools_updated event emission in setTools()

};

const onMcpUpdate = () => {
if (manager.getDiscoveryState() === MCPDiscoveryState.COMPLETED) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Interactive mode silently ignores MCP startup failures

Non-interactive paths (gemini.tsx:822, session.ts:167, acpAgent.ts:100) all call getFailedMcpServerNames() and emit Warning: MCP server(s) failed to start to stderr. The interactive AppContainer's onMcpUpdate callback has no equivalent logic. MCP failures are routed only to debugLogger (debug-mode only) and profile events (profiling-mode only) — regular interactive users see a normal UI with zero indication that their configured MCP servers failed.

Impact: Users with broken MCP servers get no visible error. The only clue is unexpected model behavior (missing tools). Extremely hard to debug without any error output.

Suggested change
if (manager.getDiscoveryState() === MCPDiscoveryState.COMPLETED) {
// In onMcpUpdate, when state transitions to COMPLETED:
if (manager.getDiscoveryState() === MCPDiscoveryState.COMPLETED) {
const failedNames = config.getFailedMcpServerNames();
if (failedNames.length > 0) {
debugLogger.warn(
`Warning: MCP server(s) failed to start: ${failedNames.join(', ')}. ` +
`Continuing with built-in tools and any servers that did connect.`,
);
}
// ... existing flushNow + finalizeOnce logic
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aaea392. The interactive now mirrors the non-interactive stderr warning — adjusted to debugLogger.warn instead of stderr because Ink owns stdout/stderr for the interactive UI and direct writes would collide with the rendered frame. Visible under QWEN_CODE_DEBUG=1 and in the debug log file, matching the channel setTools() errors already use. The surfaceFailuresOnce guard ensures the warning fires exactly once per discovery cycle (both the live onMcpUpdate COMPLETED branch and the legacy synchronous-COMPLETED branch). The MCP status footer pill continues to surface failures continuously in the UI; this log is the actionable-on-debug record.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Correcting the previous reply — shell ate one of the backticks.)

Fixed in aaea392. The interactive AppContainer now mirrors the non-interactive stderr warning — adjusted to debugLogger.warn instead of stderr because Ink owns stdout/stderr for the interactive UI and direct writes would collide with the rendered frame. Visible under QWEN_CODE_DEBUG=1 and in the debug log file, matching the channel setTools() errors already use. The surfaceFailuresOnce guard ensures the warning fires exactly once per discovery cycle (both the live onMcpUpdate COMPLETED branch and the legacy synchronous-COMPLETED branch). The MCP status footer pill continues to surface failures continuously in the UI; this log is the actionable-on-debug record.

await config.initialize();
// Same reasoning as the top-level runAcpAgent path: ACP feeds session
// messages to the model immediately, so we cannot return a Config whose
// MCP discovery is still in flight.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] ACP per-session MCP failures silently fall back to built-in-tools-only

QwenAgent.initializeConfig() (per-session ACP path) calls await config.waitForMcpReady() but does NOT call config.getFailedMcpServerNames() or emit any warning — despite the comment claiming "Same reasoning as the top-level runAcpAgent path." The top-level runAcpAgent path (lines 95-107) properly emits Warning: MCP server(s) failed to start to stderr.

Impact: ACP integrations using per-session Config instances silently lose MCP tools with zero user-visible indication. This is inconsistent with the top-level path and makes ACP MCP issues undebuggable without QWEN_CODE_DEBUG=1.

Suggested change
// MCP discovery is still in flight.
await config.waitForMcpReady();
const failedMcpServers =
typeof config.getFailedMcpServerNames === 'function'
? config.getFailedMcpServerNames()
: [];
if (failedMcpServers.length > 0) {
process.stderr.write(
`Warning: MCP server(s) failed to start: ${failedMcpServers.join(', ')}. ` +
`Continuing with built-in tools and any servers that did connect.\n`,
);
}
return config;

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aaea392 — applied the suggestion almost verbatim (kept the existing defensive typeof config.getFailedMcpServerNames === 'function' guard so tests that stub Config don't break). Per-session QwenAgent.initializeConfig() now emits the same Warning: MCP server(s) failed to start stderr line as the top-level runAcpAgent path. Added two regression tests in acpAgent.test.ts: (1) failures surface to stderr listing every failed server name, (2) stubbed Configs without getFailedMcpServerNames don't throw and don't emit a warning.

// Drop any tools that registered during the disconnect window. No-op
// if the server hadn't reached `discover()` yet, so it's safe to
// always call.
this.toolRegistry.removeMcpToolsByServer(serverName);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Health check silently reconnects MCP servers that were intentionally timed out

discoverMcpToolsForServerInternal's finally block (line 235) unconditionally calls this.startHealthCheck(serverName), even after the timeout handler has disconnected the client. The health check detects DISCONNECTED status and after ~90 seconds triggers reconnectServer(), which calls discoverMcpToolsForServer() directly — with no per-server discovery timeout guard. A server intentionally timed out for being slow will silently reconnect later, re-introducing the same pathology this PR was designed to fix.

The timeout handler here correctly calls removeMcpToolsByServer() but does not remove the client from this.clients Map or stop its health check, allowing the finally block's startHealthCheck to create a timer that will later reconnect the server.

Suggested change
this.toolRegistry.removeMcpToolsByServer(serverName);
this.toolRegistry.removeMcpToolsByServer(serverName);
// Prevent the finally block's startHealthCheck from resurrecting
// this server: remove from clients so performHealthCheck is a no-op.
this.clients.delete(serverName);
reject(

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aaea392. Good catch — this was indeed the round-6 fix's missing other half. The timeout handler now does three things instead of two: (a) await disconnect, (b) removeMcpToolsByServer, (c) stopHealthCheck + this.clients.delete(serverName). The (c) step makes performHealthCheck early-return (no client) AND ensures the finally block's startHealthCheck is now also guarded — I tightened startHealthCheck to no-op when !this.clients.has(serverName) so the trailing call is harmless even if future code adds another path to it. Regression test in mcp-client-manager.test.ts: runWithDiscoveryTimeout drops the client + stops health-check so the auto-reconnect loop cannot resurrect an intentionally timed-out server — verifies both the clients-map and healthCheckTimers-map are purged after timeout.

*/
export function recordStartupEvent(
name: string,
attrs?: StartupEventAttrs,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Silent catch {} swallows all profiler sink exceptions with no logging

The empty catch {} block silently drops all exceptions from the registered sink. If the sink throws (e.g., file write failure, corrupted internal state), profiling data is silently lost with zero debugging visibility — the worst kind of silent failure mode for an observability tool.

Suggested change
attrs?: StartupEventAttrs,
} catch (err) {
// Profiler sinks must never throw into hot paths, but we log failures.
if (typeof process !== 'undefined' && process.stderr) {
process.stderr.write(
`[startup-profiler] event sink error: ${String(err)}\n`,
);
}
}

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aaea392. Routed through debugLogger.error instead of process.stderr.write — same reasoning as the AppContainer interactive surfacing fix in the same commit: a direct stderr write here would leak to interactive users' terminals (the sink is registered when QWEN_CODE_PROFILE_STARTUP=1 is set, which is a profile/debug mode but doesn't necessarily imply the user wants stderr noise during normal runs). debugLogger is quiet by default, visible under QWEN_CODE_DEBUG=1, and written to the debug log file — matches how other "must never throw" sites in this PR (AppContainer's setTools flush, runWithDiscoveryTimeout disconnect errors) surface failures. The existing "does not bubble sink exceptions into hot paths" test still pins the contract.

…ed-out servers

Round-7 review follow-ups:

- AppContainer (interactive): MCP startup failures now route through
  debugLogger.warn on COMPLETED. Was silent — only debug logs / profile
  events surfaced failures, so regular interactive users got no
  indication their MCP servers failed. Mirrors the non-interactive
  stderr warning, adjusted to debugLogger so it doesn't collide with
  Ink's rendered output.

- acpAgent per-session: `QwenAgent.initializeConfig()` now emits the
  same `Warning: MCP server(s) failed to start` stderr line as the
  top-level `runAcpAgent` path. Previously per-session ACP configs
  with failed MCP servers silently fell back to built-in tools.

- mcp-client-manager timeout handler: after disconnecting an
  intentionally timed-out server, also drop it from `this.clients` and
  stop any pending health-check timer. Without this the discovery
  `finally` block would arm a health-check that detected DISCONNECTED
  status and called `reconnectServer()` → `discoverMcpToolsForServer()`
  directly — bypassing `runWithDiscoveryTimeout` entirely and silently
  resurrecting the slow server. `startHealthCheck` also early-returns
  for unknown servers so the trailing finally-block call is a no-op.

- startupEventSink: silent `catch {}` now logs via `debugLogger.error`
  so a corrupted sink doesn't silently drop every subsequent event.
  Quiet by default; visible under `QWEN_CODE_DEBUG=1`.

Tests:
- mcp-client-manager.test.ts: regression for the timeout → no-reconnect
  invariant (clients map purged + health-check timer absent).
- acpAgent.test.ts: per-session newSession surfaces failures to stderr,
  and stays safe when Config lacks `getFailedMcpServerNames`.

Declines (with reasoning in PR reply):
- [Critical] AppContainer batch-flush useEffect untested → re-flag of
  the round-5 deferral that wenshao acknowledged at the time. Lower-
  layer invariants (this PR's mcp-client-manager + mcp-client tests)
  pin the dependent contracts. The component-test harness for timers +
  event emitters in this file is non-trivial and out of scope; tracked
  for a follow-up.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@chiga0

chiga0 commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author

Round 7 dispositions (commit aaea392):

Fixed (3)

  • [Critical] AppContainer:599 silent interactive MCP failuresdebugLogger.warn on COMPLETED, gated by a surfaceFailuresOnce flag. Adjusted from your stderr suggestion to debugLogger to avoid colliding with Ink'''s rendered output. See thread reply.
  • [Critical] acpAgent:699 silent per-session MCP failures → applied your suggestion (kept the defensive typeof guard). Two new regression tests in acpAgent.test.ts. See thread reply.
  • [Critical/Suggestion] mcp-client-manager:634 health-check resurrection of timed-out servers → timeout handler now stopHealthCheck + this.clients.delete. startHealthCheck tightened to no-op for unknown servers. New regression test verifies both clients and healthCheckTimers are purged after timeout. See thread reply.
  • [Suggestion] startupEventSink:43 silent catch → routed through debugLogger.error (stderr would leak to interactive terminals when QWEN_CODE_PROFILE_STARTUP=1 is set in interactive mode). See thread reply.

Declined (1)

  • [Critical] AppContainer batch-flush useEffect untested — this is a re-flag of the round-5 deferral I documented (see chiga0 2026-05-12T06:33Z comment, “Deferred with reasoning” section). The deferral reasoning still stands: the AppContainer test harness mocks ~30 hooks (useGeminiStream, useTextBuffer, useKeypress, etc.) and makeFakeConfig() returns a real Config whose getToolRegistry().getMcpClientManager() isn'''t initialized — building a real timer + event-emitter harness for this effect requires wiring a fake McpClientManager through the real Config + spying through Ink'''s render loop, which is exactly the “non-trivial and prone to flakes” cost the round-5 reply called out. The lower-layer invariants this effect depends on are pinned: trailing mcp-client-update emit after COMPLETED (manager test, line 819), disconnect-on-timeout (line 763), the new no-reconnect-after-timeout test (this round), getFailedMcpServerNames filtering (config.ts), setTools() error routing (gemini-client tests). Will track a follow-up issue for the component-level test once we can extract onMcpUpdate into a testable helper without rewriting the effect.

Test-coverage suggestions

Acknowledged. Most map to lower-layer files that already have coverage (getFailedMcpServerNames is pinned in config tests; discoveryTimeoutFor httpUrl/url/non-finite have explicit tests at mcp-client-manager.test.ts line 665+; runWithDiscoveryTimeout timeout-with-disconnect test at line 763 covers the success/failure rejection paths). The remaining gaps (session.ts:146-180 profile checkpoints, gemini.tsx:396-401 sink bridge gating) are component-orchestration tests of the same complexity profile as the AppContainer one — same follow-up.

typecheck clean; mcp-client-manager (19), mcp-client (36), startupEventSink (4), AppContainer (61), acpAgent (36, +2 new), config (140), gemini (14), session (27) all green.

// eventually responds, `discover()` registers its tools into
// the live `toolRegistry` and re-emits `mcp-client-update`.
// From the user's perspective the server "failed" but its tools
// are silently active, including any that shadow built-ins.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] runWithDiscoveryTimeout timeout handler can hang if disconnect() blocks

The timeout callback executes await client.disconnect(), which internally calls await this.transport.close(). If the transport is unresponsive (which is precisely why the timeout fired — e.g., a hung stdio child process), close() may never return, causing reject() to never be called. This cascades: Promise.all(discoveryPromises) hangs → waitForMcpReady() hangs → non-interactive CLI silently blocks forever.

Suggested change
// are silently active, including any that shadow built-ins.
// Move reject() BEFORE await disconnect(), and make the cleanup fire-and-forget:
const client = this.clients.get(serverName);
reject(new Error(`MCP server '${serverName}' discovery timed out after ${timeoutMs}ms`));
if (client) {
client.disconnect().catch(() => {}); // fire-and-forget cleanup
}
this.toolRegistry.removeMcpToolsByServer(serverName);
this.stopHealthCheck(serverName);
this.clients.delete(serverName);

Alternatively, wrap disconnect() in a Promise.race with a short (e.g., 2s) safety timeout.

— DeepSeek/deepseek-v4-pro via Qwen Code /review


this.discoveryState = MCPDiscoveryState.IN_PROGRESS;
recordStartupEvent('mcp_discovery_start', {
serverCount: Object.keys(servers).length,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] serverCount in profiler events includes disabled servers

recordStartupEvent('mcp_discovery_start', { serverCount: Object.keys(servers).length }) counts all configured servers including those with disabled: true. Disabled servers are filtered out later in the per-server loop, so the reported count inflates metrics and skews per-server averages in downstream dashboards.

Suggested change
serverCount: Object.keys(servers).length,
const enabledServerNames = Object.entries(servers).filter(
([name]) => !cliConfig.isMcpServerDisabled(name),
);
recordStartupEvent('mcp_discovery_start', {
serverCount: enabledServerNames.length,
incremental: true,
});

The same fix applies to the mcp_all_servers_settled event (~line 584).

— DeepSeek/deepseek-v4-pro via Qwen Code /review

// server response time. Users can opt back into the legacy synchronous
// behavior with `QWEN_CODE_LEGACY_MCP_BLOCKING=1` — kept ≥ 1 release as
// an escape hatch.
const legacyBlockingMcp =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] QWEN_CODE_LEGACY_MCP_BLOCKING only matches === '1'

Users who set =true, =yes, or =TRUE will silently fall through to progressive mode with no warning. This is a classic env-var footgun — easy to miss during debugging.

Suggested change
const legacyBlockingMcp =
const legacyBlockingMcp =
['1', 'true', 'yes'].includes(
(process.env['QWEN_CODE_LEGACY_MCP_BLOCKING'] || '').toLowerCase(),
);

— DeepSeek/deepseek-v4-pro via Qwen Code /review

@wenshao
wenshao merged commit d343e2c into main May 13, 2026
8 checks passed
TaimoorSiddiquiOfficial added a commit to TaimoorSiddiquiOfficial/HopCode that referenced this pull request May 14, 2026
… input (QwenLM#3994)

Cherry-picked from upstream QwenLM/qwen-code commit d343e2c.
Resolved branding conflicts (QWEN_CODE_ → HOPCODE_) and skipped
writeRuntimeStatus block not exported by @hoptrendy/hopcode-core.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TaimoorSiddiquiOfficial added a commit to TaimoorSiddiquiOfficial/HopCode that referenced this pull request May 14, 2026
- client.ts: fix 'requestToSent' typo to 'requestToSend' (cherry-pick artifact)
- historyUtils.ts: add 'history_context_note' to exhaustive switch (new type from QwenLM#3994)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TaimoorSiddiquiOfficial added a commit to TaimoorSiddiquiOfficial/HopCode that referenced this pull request May 14, 2026
- fix(cli): restore settings arg to runNonInteractiveStreamJson (QwenLM#3994 regression)
- feat(skills): add bash-defensive-patterns, javascript-testing-patterns,
  modern-javascript-patterns from wshobson/agents; add zod, react-best-practices,
  composition-patterns, use-ai-sdk via autoskills
- fix(cli): correct sandboxImageUri in CLI package to use taimoorSiddiquiofficial
- chore: bump all packages to 0.27.8

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tanzhenxin added a commit that referenced this pull request May 15, 2026
…#4164)

The progressive-MCP rollout (#3994) regressed non-interactive MCP tool
visibility on the first `--prompt` request — the model never sees the
configured MCP tool and answers from its own knowledge, so the test's
`waitForToolCall('mcp__addition-server__add')` assertion times out on
all three retries. Reproduced locally: 167s 3/3-fail without the
rollback flag, 22s pass with it.

Set `QWEN_CODE_LEGACY_MCP_BLOCKING=1` in the test's `beforeAll` so the
spawned CLI uses the pre-#3994 synchronous discovery path. Scoped to
this single test rather than the workflow env so other integration
tests keep exercising the new progressive-MCP code path.

Temporary workaround. Remove once #4163 is fixed.
TaimoorSiddiquiOfficial pushed a commit to TaimoorSiddiquiOfficial/HopCode that referenced this pull request May 15, 2026
…QwenLM#4164)

The progressive-MCP rollout (QwenLM#3994) regressed non-interactive MCP tool
visibility on the first `--prompt` request — the model never sees the
configured MCP tool and answers from its own knowledge, so the test's
`waitForToolCall('mcp__addition-server__add')` assertion times out on
all three retries. Reproduced locally: 167s 3/3-fail without the
rollback flag, 22s pass with it.

Set `QWEN_CODE_LEGACY_MCP_BLOCKING=1` in the test's `beforeAll` so the
spawned CLI uses the pre-QwenLM#3994 synchronous discovery path. Scoped to
this single test rather than the workflow env so other integration
tests keep exercising the new progressive-MCP code path.

Temporary workaround. Remove once QwenLM#4163 is fixed.

(cherry picked from commit fa6f664)
chiga0 added a commit that referenced this pull request May 15, 2026
… tools reach the model (#4166)

* fix(core): refresh systemInstruction in setTools() so progressive MCP tools reach the model

Under PR #3994's progressive MCP path, Config.initialize() runs
startChat() BEFORE MCP discovery starts, then kicks discovery off in the
background and re-runs setTools() once it settles. But setTools() only
updated chat.generationConfig.tools — not systemInstruction — and MCP
tools are shouldDefer=true, so they were filtered out of declarations
anyway. The prompt's "Deferred Tools" listing was frozen at the
built-in-only snapshot from the initial startChat(), and the model had
no signal that any MCP tool existed. Headless --prompt runs silently
regressed to built-ins (issue #4163); interactive mode had the same gap
but was masked by retries.

setTools() now rebuilds the system instruction with the up-to-date
deferred summary and re-binds it to the live chat. The eager-reveal
guard for "ToolSearch unavailable + deferred tools present" moves with
it so a freshly-arrived MCP tool in `--exclude-tools tool_search`
sessions still lands in declarations instead of disappearing silently.
Shared with startChat() / refreshSystemInstruction() via a new private
resolveDeferredToolsForSystemPrompt() helper so the three paths cannot
drift apart again.

The legacy synchronous path (QWEN_CODE_LEGACY_MCP_BLOCKING=1) was
incidentally correct because discovery happened before startChat(); it
remains correct.

Test plan:
- packages/core/src/core/client.test.ts — three new cases covering
  newly-arrived MCP tools, already-revealed filtering, and the
  no-ToolSearch eager-reveal path.
- Full client.test.ts (107 tests) green.
- tool-search / skill-manager / agent / mcp-client-manager / AppContainer
  test suites green (callers of setTools()).
- CI integration: integration-tests/cli/simple-mcp-server.test.ts is
  expected to pass on first try without QWEN_CODE_LEGACY_MCP_BLOCKING.

Fixes #4163

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(core): lock in SessionStart preservation across setTools refresh

Adds the regression test chiga0 asked for in the PR #4166 review:
proves that setTools()'s setSystemInstruction-then-reapply pattern keeps
the SessionStart hook's additionalContext intact, so progressive-MCP
refreshes (AppContainer batch flush + the trailing setTools after
waitForMcpReady) don't silently strip hook context from the system
instruction.

Generated by claude-opus-4-7

Co-authored-by: Claude <claude-opus-4-7@anthropic.com>

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Claude <claude-opus-4-7@anthropic.com>
jdmanring pushed a commit to jdmanring/qwen-code that referenced this pull request Jul 2, 2026
Addresses review feedback on PR QwenLM#3994:

- Skip user-disabled servers in discoverAllMcpToolsIncremental. The new
  incremental path used to iterate Object.entries(servers) without
  consulting isMcpServerDisabled, so a server the user had explicitly
  turned off would still get connected and its tools registered.
  Mirrors the existing protection in discoverAllMcpTools.

- Disconnect the underlying client when runWithDiscoveryTimeout fires.
  Without this, the inner discoverMcpToolsForServer kept running after
  the timeout rejected the outer promise — if discover() eventually
  succeeded it would register the late server's tools into the live
  toolRegistry (a silent registration vector, especially exploitable
  with a 0/negative discoveryTimeoutMs override).

- Clamp discoveryTimeoutMs to [100ms, 300_000ms]. 0/negative/Infinity
  values previously passed through to setTimeout unvalidated and made
  the silent-registration bug above trivially reachable.

- Classify the `tcp` (WebSocket) transport field as remote so hung WS
  handshakes use the 5s default instead of the 30s stdio default.

- Defensive delete of serverDiscoveryPromises[name] in the per-server
  catch so a doomed/orphan entry can't briefly short-circuit a
  subsequent discoverMcpToolsForServer call.

Adds focused tests for each fix.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

# Conflicts:
#	packages/core/src/tools/mcp-client-manager.test.ts
#	packages/core/src/tools/mcp-client-manager.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/feature-request New feature or enhancement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants