Skip to content

feat(extensions): interactive multi-tab /extensions manager (Installed / Discover / Sources) - #4850

Merged
wenshao merged 51 commits into
mainfrom
feat/enhance-extensions-command
Jun 20, 2026
Merged

feat(extensions): interactive multi-tab /extensions manager (Installed / Discover / Sources)#4850
wenshao merged 51 commits into
mainfrom
feat/enhance-extensions-command

Conversation

@BZ-D

@BZ-D BZ-D commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Turns /extensions into an interactive, multi-tab manager instead of a flat read-only list. Three tabs — Installed, Discover, Sources — cover the full lifecycle of finding, installing, configuring and removing extensions (and standalone MCP servers).

Installed

  • Grouped list (Favorites / User level / Project level / Local level / Disabled) of installed extensions and standalone MCP servers.
  • Extension detail with a full action menu: Enable/Disable, Add/Remove Favorite, Change scope (Global / Project / Local), Mark for Update / Update Now, and Uninstall (with confirmation).
  • The MCP-server detail reuses the /mcp dialog components (ServerDetailStep / ToolListStep / AuthenticateStep), so an installed MCP server shows live connection status and offers View tools, Enable/Disable, Reconnect, Re-authenticate and Clear authentication. Extension-bundled MCP servers nest under their parent extension and can be disabled per-server.

Discover

  • Browse installable extensions from the configured marketplace sources, with type-to-search filtering, a windowed list (capped at 6 visible items with scroll hints), per-item detail, and install at a chosen scope (user / project). Ctrl+R re-fetches the listings.
  • A network fetch timeout (and now a hard wall-clock deadline + response-size cap) keeps discovery from hanging on slow/unreachable/hostile sources.

Sources

  • Entry points to Install new extension and Add new marketplace, above the installed Extensions and Marketplaces sections. Marketplace detail offers Browse (jumps to Discover filtered to that source), Update, and Remove. Also exposed as the qwen extensions sources add/list/update/remove CLI group.

Core

  • Marketplace registry & discovery: resolve sources from owner/repo, https/git URLs, git@ SSH, scoped npm packages, local paths, and Claude marketplace manifests (.claude-plugin/marketplace.json).
  • Extension preferences (extensionPreferences.ts): persisted favorites and per-extension scope intent that drive the Installed-tab grouping.
  • Standalone Claude-plugin install from a git URL, plus --scope on extensions install.

i18n

  • Full Simplified Chinese (zh), Traditional Chinese (zh-TW) and English (en) translations for all new strings; key parity is enforced by npm run check-i18n.

Why it's needed

The previous /extensions was a read-only list — installing, enabling/disabling, re-scoping, updating, removing, browsing marketplaces, and managing extension-bundled MCP servers all required separate CLI commands or weren't possible at runtime. This consolidates the whole extension lifecycle into one hot-reloading TUI aligned with the project's extensions/marketplace roadmap and with the existing /mcp manager (whose components it reuses), so users can discover and manage Gemini- and Claude-format extensions without leaving the session.

Reviewer Test Plan

How to verify

  1. npm run build, launch the CLI, run /extensions (or /extensions manage).
  2. Discover: with a marketplace source configured, type to filter, Enter for detail, install at a chosen scope, Ctrl+R to refresh. Confirm the installed plugin is converted to qwen-extension.json and its commands/skills become available.
  3. Installed: Space enable/disable, f favorite, Enter action menu (change scope / mark update / uninstall). Confirm groups re-sort and favorites/scope/enablement persist across restart. Open an MCP server's detail and confirm live status + View tools / Reconnect / auth actions.
  4. Sources: add a marketplace (owner/repo, git/https URL, or local path), Browse → Discover filtered to it, Update, Remove. Confirm ~/.qwen/extensions/marketplaces.json is updated.
  5. CLI parity: qwen extensions sources add/list/update/remove and qwen extensions install <src> --scope project.

Evidence (Before & After)

  • Verified locally (macOS, this change set): the affected unit suites — claude-converter, marketplace, sourceRegistry (61 tests) and the full cli extensions suites incl. ExtensionsManagerDialog and sources (152 tests) — pass, plus prettier/eslint/tsc on the changed files. A full TUI E2E was not run locally.
  • Collaborator verification (Linux): @wenshao ran a tmux real-user E2E pass (all three tabs, install/uninstall/enable-disable across restarts, scope changes, MCP toggle, the CLI command group, zh rendering) and reported it passing — see feat(extensions): interactive multi-tab /extensions manager (Installed / Discover / Sources) #4850 (comment).

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux

macOS ✅ = unit/lint/type checks for the changed code, run locally this round. Linux ✅ = collaborator E2E (linked above). Windows ⚠️ = not manually verified locally; relying on CI (ubuntu/macos/windows unit + CodeQL green).

Environment (optional)

npm run build + built CLI in a tmux TUI session (collaborator); unit tests via vitest (local, macOS).

Risk & Scope

  • Main risk / tradeoff: primarily a UI/management layer plus a new marketplace-discovery path. The latter now fetches arbitrary user-supplied URLs and renders untrusted marketplace metadata, so the review hardening in 05eb44f6 (ANSI sanitization, path-traversal confinement, fetch size/time caps, homepage scheme check) is the security-relevant part to scrutinize.
  • Not validated / out of scope: does not change the underlying install or format-conversion logic; git-subdir install from an http-type marketplace is a known gap (clear error, not a wrong install) tracked as a follow-up.
  • Breaking changes / migration: none.

Linked Issues

N/A — no tracking issue; aligns with the extensions/marketplace roadmap.

克竟 added 2 commits June 8, 2026 17:36
…rketplaces)

Upgrade the /extensions management dialog from a linear wizard into a
multi-tab interactive dialog aligned with Claude Code's /plugin command.

UI (packages/cli):
- Discover: pull installable plugins from configured marketplaces,
  multi-select (Space), batch install (i) with Global/Project/Local scope,
  open homepage, per-plugin details.
- Installed: plugins + standalone MCP servers grouped by
  Favorites/Local/User/Project/Disabled; Space toggles enable/disable,
  f toggles favorite, Enter opens details with an action menu
  (toggle/favorite/mark-for-update/update/uninstall).
- Marketplaces: add/list/view/remove marketplace sources
  (owner/repo, SSH, HTTP JSON, local path).
- Tabbed shell with Tab/arrow switching and a focus-lock contract so a tab
  owns Escape while in a sub-view.

Core (packages/core):
- ExtensionPreferencesStore: favorites + per-extension scope intent.
- MarketplaceRegistryStore + discoverPlugins(): persistent marketplace
  source registry and cross-source discovery.
- loadMarketplaceConfigFromSource() in marketplace.ts (GitHub/local/HTTP-JSON).
- ExtensionManager methods for marketplaces, discovery, favorites and scopes;
  preference cleanup on uninstall.

Scope mapping: Global -> User; Project/Local -> workspace-scoped enablement
(install then re-scope so the choice actually restricts where it is active).

The Errors tab is intentionally deferred per the spec.

Tests: 19 core unit tests (preferences/registry/discovery) and 11 tabbed
dialog integration tests; existing extension suites updated. typecheck,
lint and i18n checks pass.
Rework the Discover tab's Enter detail view to match Claude Code's
"Plugin details" page in both layout and interaction:

- Layout: "Plugin details" header, title, "from <marketplace>", last
  updated / version, description, "By: <author>", a "Will install:"
  component summary (Skills/Commands/Agents/MCP servers), and a trust
  warning.
- Interaction: the scope choice is now an inline action selector on the
  detail page (Install for you / for all collaborators / in this repo
  only / Open homepage / Back to plugin list), selected with Enter —
  replacing the previous i/h shortcuts and the separate scope step.
- Footer shows "Enter to select · Esc to go back" while a tab sub-view
  is open.

Core: DiscoveredPlugin now carries declared `components` and a
best-effort `lastUpdated`, surfaced by discoverPlugins().

Adds a core test for component/lastUpdated extraction and a UI test for
the detail layout + inline selector.
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Summary

Package Lines Statements Functions Branches
CLI 77.3% 77.3% 80.82% 79.78%
Core 82.94% 82.94% 84.61% 84.35%
CLI Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |    77.3 |    79.78 |   80.82 |    77.3 |                   
 src               |   71.72 |    68.93 |   73.91 |   71.72 |                   
  gemini.tsx       |   63.93 |    66.07 |   71.42 |   63.93 | ...1229,1234-1239 
  ...ractiveCli.ts |   71.46 |    66.11 |   66.66 |   71.46 | ...1668-1670,1705 
  ...liCommands.ts |   84.88 |    77.92 |     100 |   84.88 | ...41,367,401,493 
  ...ActiveAuth.ts |     100 |     87.5 |     100 |     100 | 66-80             
 ...cp-integration |   56.51 |    62.15 |   82.75 |   56.51 |                   
  acpAgent.ts      |   56.37 |    62.19 |   82.94 |   56.37 | ...7127,7152-7167 
  authMethods.ts   |      92 |       60 |     100 |      92 | 33-34             
  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 |   85.55 |    78.59 |    88.8 |   85.55 |                   
  ...ryReplayer.ts |   67.34 |     75.6 |   81.81 |   67.34 | ...54-269,282-283 
  Session.ts       |   86.26 |    78.14 |   90.09 |   86.26 | ...4674,4700-4704 
  ...entTracker.ts |   91.39 |    89.47 |   88.88 |   91.39 | ...31,195,266-275 
  index.ts         |       0 |        0 |       0 |       0 | 1-40              
  ...ssionUtils.ts |   84.21 |    83.33 |     100 |   84.21 | ...37-153,209-211 
  tasksSnapshot.ts |   94.06 |    86.66 |     100 |   94.06 | 60-66             
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...ssion/emitters |   96.25 |    94.11 |   96.96 |   96.25 |                   
  BaseEmitter.ts   |    92.3 |    81.81 |     100 |    92.3 | 23-24             
  ...ageEmitter.ts |   95.23 |    95.12 |     100 |   95.23 | 48-55             
  PlanEmitter.ts   |     100 |      100 |     100 |     100 |                   
  ...allEmitter.ts |   98.44 |    94.44 |     100 |   98.44 | 318-319,420,428   
  index.ts         |       0 |        0 |       0 |       0 | 1-10              
 ...ession/rewrite |    91.3 |    88.09 |   94.44 |    91.3 |                   
  LlmRewriter.ts   |      81 |       84 |     100 |      81 | ...,88-89,155-159 
  ...Middleware.ts |   96.74 |    86.84 |     100 |   96.74 | 135,143-145       
  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/commands      |      62 |    89.28 |      50 |      62 |                   
  auth.ts          |     100 |    83.33 |     100 |     100 | 11,14             
  channel.ts       |   56.66 |      100 |       0 |   56.66 | 15-19,27-34       
  extensions.tsx   |   96.77 |      100 |      50 |   96.77 | 39                
  hooks.tsx        |   66.66 |      100 |       0 |   66.66 | 20-24             
  mcp.ts           |   95.45 |      100 |      50 |   95.45 | 31                
  review.ts        |   51.85 |      100 |       0 |   51.85 | 24-35,38          
  serve.ts         |   51.35 |     92.3 |      50 |   51.35 | 31-33,317-528     
  sessions.ts      |     100 |      100 |      50 |     100 |                   
 ...mmands/channel |   40.73 |    84.26 |      55 |   40.73 |                   
  ...l-registry.ts |    6.66 |      100 |       0 |    6.66 | 6-32,35-53        
  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       |   97.11 |    94.73 |     100 |   97.11 | 27-28,45          
  start.ts         |   31.94 |    53.84 |   71.42 |   31.94 | ...83-486,495-497 
  status.ts        |   17.85 |      100 |       0 |   17.85 | 15-26,32-76       
  stop.ts          |      20 |      100 |       0 |      20 | 14-48             
 ...nds/extensions |   86.93 |    88.88 |   84.74 |   86.93 |                   
  consent.ts       |   72.82 |       90 |   42.85 |   72.82 | ...86-142,157-163 
  disable.ts       |     100 |      100 |     100 |     100 |                   
  enable.ts        |     100 |      100 |     100 |     100 |                   
  install.ts       |   81.43 |    79.41 |      75 |   81.43 | ...98-201,204-213 
  link.ts          |     100 |      100 |     100 |     100 |                   
  list.ts          |     100 |     87.5 |     100 |     100 | 22                
  new.ts           |     100 |      100 |     100 |     100 |                   
  settings.ts      |   99.15 |      100 |   83.33 |   99.15 | 151               
  sources.ts       |   93.42 |    87.09 |   92.85 |   93.42 | ...4-66,96-98,167 
  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         |   68.47 |    33.33 |     100 |   68.47 | ...98-102,104-108 
 ...les/mcp-server |       0 |        0 |       0 |       0 |                   
  example.ts       |       0 |        0 |       0 |       0 | 1-60              
 ...amples/starter |       0 |        0 |       0 |       0 |                   
  example.ts       |       0 |        0 |       0 |       0 | 1-64              
 src/commands/mcp  |   90.09 |    86.23 |   83.33 |   90.09 |                   
  add.ts           |    99.3 |    96.07 |     100 |    99.3 | 154-155           
  approve.ts       |   76.19 |     87.5 |   66.66 |   76.19 | ...,89-99,114-124 
  list.ts          |   92.59 |    83.87 |      80 |   92.59 | ...62-164,180-181 
  reconnect.ts     |    78.1 |    73.33 |   85.71 |    78.1 | 42-55,163-185     
  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 
 ...mands/sessions |   91.56 |    86.95 |   83.33 |   91.56 |                   
  common.ts        |     100 |      100 |     100 |     100 |                   
  list.ts          |   90.96 |    86.66 |   81.81 |   90.96 | 208-219,221-222   
 src/config        |   91.79 |    85.88 |   92.94 |   91.79 |                   
  auth.ts          |   88.94 |    83.56 |     100 |   88.94 | ...86-287,303-304 
  ...eMcpImport.ts |   87.88 |    81.52 |     100 |   87.88 | ...57-365,447-448 
  config.ts        |   87.25 |     84.2 |   82.75 |   87.25 | ...2059,2061-2069 
  keyBindings.ts   |   97.01 |       50 |     100 |   97.01 | 207-210           
  ...ngsAdapter.ts |     100 |    94.11 |     100 |     100 | 64                
  mcpApprovals.ts  |   96.12 |    94.87 |     100 |   96.12 | 193-194,199-201   
  mcpJson.ts       |     100 |      100 |     100 |     100 |                   
  mcpServers.ts    |   92.85 |     87.5 |     100 |   92.85 | 46-47             
  ...idersScope.ts |      92 |       90 |     100 |      92 | 11-12             
  ...abledTools.ts |     100 |      100 |     100 |     100 |                   
  sandboxConfig.ts |   61.64 |    71.87 |   66.66 |   61.64 | ...54-68,73,77-89 
  settings.ts      |   78.96 |       87 |    87.8 |   78.96 | ...1532,1547-1550 
  ...ingsSchema.ts |     100 |      100 |     100 |     100 |                   
  ...ngsWatcher.ts |   95.54 |    88.34 |     100 |   95.54 | ...28,277-278,293 
  ...tedFolders.ts |   96.89 |    93.65 |     100 |   96.89 | ...39-241,256-257 
 ...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.06 |     100 |   94.74 |                   
  ...-v2-shared.ts |     100 |      100 |     100 |     100 |                   
  v1-to-v2.ts      |   81.75 |    90.56 |     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    |   69.39 |    66.66 |   63.15 |   69.39 |                   
  ...tputBridge.ts |   69.48 |     67.3 |    64.7 |   69.48 | ...82-383,391-394 
  ...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.12 |       80 |   67.56 |   84.12 |                   
  index.ts         |   67.45 |    76.92 |   57.14 |   67.45 | ...70-271,294-299 
  languages.ts     |   96.92 |    86.66 |     100 |   96.92 | 134-135,167,184   
  ...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.45 |    71.03 |   74.07 |   72.45 |                   
  session.ts       |   76.46 |    69.34 |   85.71 |   76.46 | ...32-833,842-852 
  types.ts         |    42.5 |      100 |   33.33 |    42.5 | ...90-591,594-595 
 ...active/control |   76.29 |    88.23 |      80 |   76.29 |                   
  ...rolContext.ts |    6.89 |        0 |       0 |    6.89 | 50-86             
  ...Dispatcher.ts |   91.66 |    91.83 |   88.88 |   91.66 | ...49-367,383,386 
  ...rolService.ts |     7.4 |        0 |       0 |     7.4 | 46-185            
 ...ol/controllers |    26.7 |    37.93 |   35.48 |    26.7 |                   
  ...Controller.ts |   36.97 |       80 |      80 |   36.97 | ...15-117,127-210 
  ...Controller.ts |       0 |        0 |       0 |       0 | 1-56              
  ...Controller.ts |   31.32 |     38.7 |      40 |   31.32 | ...68-577,592-597 
  ...Controller.ts |   14.06 |      100 |       0 |   14.06 | ...82-117,130-133 
  ...Controller.ts |   21.97 |    28.57 |   27.27 |   21.97 | ...39-451,460-489 
 .../control/types |       0 |        0 |       0 |       0 |                   
  serviceAPIs.ts   |       0 |        0 |       0 |       0 | 1                 
 ...Interactive/io |   98.01 |    93.79 |   95.23 |   98.01 |                   
  ...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.38 |      100 |   90.47 |   98.38 | 83-84,124-125     
  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.44 |    81.33 |   81.14 |   80.44 |                   
  ...sionBridge.ts |     100 |      100 |     100 |     100 |                   
  auth.ts          |   93.26 |    92.64 |     100 |   93.26 | ...07-308,311-313 
  ...temAdapter.ts |     100 |      100 |     100 |     100 |                   
  capabilities.ts  |     100 |    95.45 |     100 |     100 | 342               
  daemonLogger.ts  |   98.63 |    90.32 |   95.83 |   98.63 | 161,165           
  daemonStatus.ts  |    98.3 |    82.99 |     100 |    98.3 | ...11,513-514,563 
  ...usProvider.ts |   67.01 |    51.42 |     100 |   67.01 | ...40-245,278-286 
  debugMode.ts     |     100 |      100 |     100 |     100 |                   
  demo.ts          |     100 |      100 |     100 |     100 |                   
  envSnapshot.ts   |   92.75 |       84 |     100 |   92.75 | 110-113,179-186   
  eventBus.ts      |     100 |      100 |     100 |     100 |                   
  ...oryChannel.ts |       0 |        0 |       0 |       0 | 1-14              
  index.ts         |       0 |        0 |       0 |       0 | 1-143             
  loopbackBinds.ts |     100 |      100 |     100 |     100 |                   
  ...ssionAudit.ts |     100 |      100 |   93.33 |     100 |                   
  rateLimit.ts     |   90.37 |    87.77 |   93.75 |   90.37 | ...95-297,348-352 
  runQwenServe.ts  |   69.08 |    81.87 |   31.37 |   69.08 | ...1454,1457-1464 
  server.ts        |   81.22 |    82.01 |   84.94 |   81.22 | ...5592,5658-5667 
  status.ts        |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...hellStatic.ts |   89.18 |       80 |     100 |   89.18 | ...79-182,215-218 
  ...paceAgents.ts |   62.47 |    70.34 |   90.47 |   62.47 | ...1346,1356-1366 
  ...paceMemory.ts |   87.13 |    78.46 |     100 |   87.13 | ...54-361,421-428 
 src/serve/acpHttp |   66.04 |     67.9 |   93.61 |   66.04 |                   
  ...onRegistry.ts |   89.08 |    82.43 |   93.33 |   89.08 | ...05,479,499-513 
  dispatch.ts      |   56.25 |    58.89 |     100 |   56.25 | ...2469,2543-2546 
  index.ts         |   75.63 |    68.21 |    90.9 |   75.63 | ...31,734,760-762 
  jsonRpc.ts       |     100 |    96.96 |     100 |     100 | 92                
  sseStream.ts     |   93.91 |    87.87 |   84.61 |   93.91 | ...50-152,154-156 
  ...portStream.ts |       0 |        0 |       0 |       0 | 1                 
  wsStream.ts      |   91.86 |       80 |     100 |   91.86 | 45,50,93,97-100   
 src/serve/auth    |   86.86 |    79.18 |   93.87 |   86.86 |                   
  deviceFlow.ts    |   96.35 |       80 |   97.61 |   96.35 | ...1358,1453,1519 
  ...owProvider.ts |   44.24 |    74.07 |   71.42 |   44.24 | ...23-284,297,301 
 src/serve/fs      |   85.12 |    81.01 |     100 |   85.12 |                   
  audit.ts         |     100 |    96.15 |     100 |     100 | 201               
  errors.ts        |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  paths.ts         |   77.82 |    77.08 |     100 |   77.82 | ...64,493-497,510 
  policy.ts        |   90.32 |    89.18 |     100 |   90.32 | 142-150           
  ...FileSystem.ts |   84.03 |    78.55 |     100 |   84.03 | ...2031,2058-2059 
 src/serve/routes  |   75.89 |    76.51 |   94.28 |   75.89 |                   
  a2uiAction.ts    |     100 |    93.65 |     100 |     100 | 114-118,163,267   
  ...ceFileRead.ts |   94.41 |    76.92 |     100 |   94.41 | ...28-329,390-392 
  ...eFileWrite.ts |    82.1 |    60.52 |     100 |    82.1 | ...42-244,247-249 
  ...ceSettings.ts |   23.62 |      100 |      50 |   23.62 | ...10-223,230-327 
 ...kspace-service |   81.62 |     82.3 |    87.5 |   81.62 |                   
  index.ts         |   81.79 |    83.03 |   93.33 |   81.79 | ...93-498,558-623 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 src/services      |   91.96 |    90.65 |   97.56 |   91.96 |                   
  ...mandLoader.ts |     100 |    88.88 |     100 |     100 | 98-111            
  ...killLoader.ts |     100 |    93.33 |     100 |     100 | 48,67             
  ...andService.ts |   98.73 |      100 |     100 |   98.73 | 107               
  ...mandLoader.ts |   86.83 |    83.87 |     100 |   86.83 | ...30-335,340-345 
  ...omptLoader.ts |   75.84 |    80.64 |   83.33 |   75.84 | ...10-211,277-278 
  ...mandLoader.ts |     100 |    97.14 |     100 |     100 | 66                
  ...nd-factory.ts |   91.42 |    91.66 |     100 |   91.42 | 128,137-144       
  ...ation-tool.ts |     100 |    95.45 |     100 |     100 | 125               
  ...ndMetadata.ts |   98.21 |    96.66 |     100 |   98.21 | 83,87             
  commandUtils.ts  |      96 |     90.9 |     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 |    88.3 |    85.49 |   92.59 |    88.3 |                   
  DataProcessor.ts |   88.22 |    85.48 |      95 |   88.22 | ...1341,1345-1352 
  ...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 |    84.84 |     100 |   97.35 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  tipHistory.ts    |   92.59 |       70 |     100 |   92.59 | ...24,146,153,162 
  tipRegistry.ts   |     100 |      100 |     100 |     100 |                   
  tipScheduler.ts  |     100 |    91.66 |     100 |     100 | 55                
 src/startup       |   80.53 |     74.6 |     100 |   80.53 |                   
  ...reeStartup.ts |   80.53 |     74.6 |     100 |   80.53 | ...94,403,409-412 
 src/test-utils    |   93.78 |    83.33 |      80 |   93.78 |                   
  ...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            |   70.29 |    73.78 |   60.93 |   70.29 |                   
  App.tsx          |   33.33 |       75 |   33.33 |   33.33 | 32-86             
  AppContainer.tsx |   70.83 |    67.34 |      55 |   70.83 | ...3356,3360-3364 
  ...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        |      60 |      100 |   35.29 |      60 | ...52,54-55,60-61 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  keyMatchers.ts   |   95.91 |    97.14 |     100 |   95.91 | 25-26             
  ...tic-colors.ts |     100 |      100 |     100 |     100 |                   
  ...inePresets.ts |   98.28 |    89.87 |     100 |   98.28 | ...34,261,420-422 
  textConstants.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/auth       |   59.21 |    65.94 |   51.11 |   59.21 |                   
  AuthDialog.tsx   |   62.87 |     42.1 |   18.18 |   62.87 | ...03,310-332,336 
  ...nProgress.tsx |       0 |        0 |       0 |       0 | 1-64              
  ...etupSteps.tsx |   60.03 |    70.37 |      56 |   60.03 | ...87,791,800,803 
  useAuth.ts       |    94.6 |    73.52 |     100 |    94.6 | ...21-222,241-247 
  ...rSetupFlow.ts |   43.52 |    33.33 |      50 |   43.52 | ...72-393,410-453 
 src/ui/commands   |   78.24 |     81.5 |   86.99 |   78.24 |                   
  aboutCommand.ts  |     100 |      100 |     100 |     100 |                   
  agentsCommand.ts |   83.78 |      100 |      60 |   83.78 | 30-32,42-44       
  ...odeCommand.ts |   89.47 |    81.25 |     100 |   89.47 | 92-93,95-100      
  arenaCommand.ts  |   62.81 |    58.73 |   65.21 |   62.81 | ...90-595,680-688 
  authCommand.ts   |     100 |      100 |     100 |     100 |                   
  branchCommand.ts |     100 |      100 |     100 |     100 |                   
  btwCommand.ts    |   94.32 |    77.41 |     100 |   94.32 | 35-36,114-119     
  bugCommand.ts    |     100 |    77.77 |     100 |     100 | 27,61             
  cdCommand.ts     |   89.44 |    80.35 |     100 |   89.44 | ...81,106-111,190 
  clearCommand.ts  |   79.64 |       68 |     100 |   79.64 | ...24-125,133-142 
  ...essCommand.ts |   67.95 |    55.88 |      75 |   67.95 | ...86-187,201-204 
  ...astCommand.ts |   70.86 |    74.07 |      75 |   70.86 | ...,61-93,117-122 
  ...extCommand.ts |   65.35 |     66.1 |   84.61 |   65.35 | ...42-575,586-587 
  copyCommand.ts   |   98.49 |    95.78 |     100 |   98.49 | ...80,280,321,327 
  deleteCommand.ts |     100 |      100 |     100 |     100 |                   
  diffCommand.ts   |     100 |     87.5 |     100 |     100 | ...61,224-225,238 
  ...ryCommand.tsx |   81.84 |    86.11 |   91.66 |   81.84 | ...66-271,318-325 
  docsCommand.ts   |     100 |     90.9 |     100 |     100 | 25                
  doctorCommand.ts |   61.27 |    87.06 |    87.5 |   61.27 | ...71-372,445-665 
  dreamCommand.ts  |   85.45 |    88.88 |     100 |   85.45 | 58-65             
  editorCommand.ts |     100 |      100 |     100 |     100 |                   
  exportCommand.ts |   98.25 |    91.02 |     100 |   98.25 | ...81,198-199,364 
  ...onsCommand.ts |    50.3 |    48.14 |   69.23 |    50.3 | ...08,262-314,375 
  forgetCommand.ts |     100 |       90 |     100 |     100 | 59                
  forkCommand.ts   |     100 |    94.11 |     100 |     100 | 96,147            
  goalCommand.ts   |   91.46 |    84.44 |      90 |   91.46 | ...87-190,202-205 
  helpCommand.ts   |     100 |      100 |     100 |     100 |                   
  ...oryCommand.ts |     100 |      100 |     100 |     100 |                   
  hooksCommand.ts  |   81.13 |    65.71 |   85.71 |   81.13 | ...,86-93,131-132 
  ideCommand.ts    |   60.75 |    64.28 |   41.17 |   60.75 | ...05-306,310-324 
  ...figCommand.ts |   52.83 |    81.25 |      70 |   52.83 | ...74-319,321-330 
  initCommand.ts   |   84.33 |    72.72 |     100 |   84.33 | 68,82-87,89-94    
  ...ghtCommand.ts |   77.87 |    71.42 |     100 |   77.87 | ...44-245,250-272 
  ...ageCommand.ts |   92.17 |    82.69 |     100 |   92.17 | ...39,159,168-178 
  lspCommand.ts    |     100 |    86.95 |     100 |     100 | 31,101-102        
  mcpCommand.ts    |     100 |      100 |     100 |     100 |                   
  memoryCommand.ts |     100 |      100 |     100 |     100 |                   
  modelCommand.ts  |   78.22 |    80.35 |      75 |   78.22 | ...20-225,277-282 
  ...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 |      96 |       70 |     100 |      96 | 57,62             
  renameCommand.ts |   85.71 |    86.04 |     100 |   85.71 | ...02-209,216-221 
  ...oreCommand.ts |    90.9 |    86.04 |     100 |    90.9 | ...41-146,176-177 
  resumeCommand.ts |     100 |      100 |     100 |     100 |                   
  rewindCommand.ts |   81.25 |      100 |      50 |   81.25 | 20-22             
  ...ngsCommand.ts |     100 |      100 |     100 |     100 |                   
  ...hubCommand.ts |   81.43 |    65.21 |      80 |   81.43 | ...70-173,176-179 
  skillsCommand.ts |    85.5 |    81.25 |     100 |    85.5 | 36-44,70          
  statsCommand.ts  |   90.15 |    78.53 |     100 |   90.15 | ...55-658,749-756 
  ...ineCommand.ts |     100 |      100 |     100 |     100 |                   
  ...aryCommand.ts |    6.43 |      100 |      50 |    6.43 | 31-330            
  tasksCommand.ts  |   77.22 |    72.13 |     100 |   77.22 | ...46-150,172-177 
  ...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             
  ...owsCommand.ts |   89.82 |    87.03 |      60 |   89.82 | ...23-124,133-138 
 src/ui/components |   64.71 |    77.56 |   63.13 |   64.71 |                   
  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    |    88.7 |       75 |     100 |    88.7 | 36,38-43,45       
  ...odeDialog.tsx |   87.24 |    72.22 |   33.33 |   87.24 | ...85,233-238,245 
  AsciiArt.ts      |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |   16.27 |      100 |       0 |   16.27 | 19-58             
  ...TextInput.tsx |    83.8 |    76.92 |     100 |    83.8 | ...38,252-254,356 
  Composer.tsx     |   94.39 |    66.66 |     100 |   94.39 | ...-71,83,138,151 
  ...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 |   11.64 |      100 |       0 |   11.64 | 69-562            
  DiffDialog.tsx   |   31.17 |    19.51 |   30.76 |   31.17 | ...07-712,722-735 
  ...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       |   78.12 |    51.16 |     100 |   78.12 | ...43,176,198-203 
  ...ngSpinner.tsx |   68.42 |       80 |      50 |   68.42 | 35-52,73,80-81    
  GoalPill.tsx     |   76.19 |    81.81 |     100 |   76.19 | 24-30,46-50       
  Header.tsx       |   98.62 |    94.28 |     100 |   98.62 | 162,164           
  Help.tsx         |   98.32 |       90 |     100 |   98.32 | ...24,381,447-448 
  ...emDisplay.tsx |   71.42 |    58.33 |     100 |   71.42 | ...74,377,380-386 
  ...ngeDialog.tsx |     100 |      100 |     100 |     100 |                   
  InputPrompt.tsx  |   85.03 |    80.45 |     100 |   85.03 | ...1698,1724,1771 
  ...Shortcuts.tsx |   20.87 |      100 |       0 |   20.87 | ...6,49-51,67-125 
  ...Indicator.tsx |   98.14 |    97.82 |     100 |   98.14 | 157-158           
  ...firmation.tsx |   91.42 |      100 |      50 |   91.42 | 26-31             
  MainContent.tsx  |   87.36 |     87.2 |   66.66 |   87.36 | ...20-321,380-384 
  MemoryDialog.tsx |   61.87 |    76.05 |    62.5 |   61.87 | ...72,391,428-430 
  ...geDisplay.tsx |       0 |        0 |       0 |       0 | 1-41              
  ModelDialog.tsx  |   85.31 |    70.42 |     100 |   85.31 | ...03-619,676-680 
  ...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 |   92.64 |    85.71 |     100 |   92.64 | 102-106,134-139   
  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 |   92.79 |    82.65 |     100 |   92.79 | ...19-323,354-370 
  ...ionPicker.tsx |   83.66 |    72.13 |     100 |   83.66 | ...96,402,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 
  ...tedDialog.tsx |     100 |      100 |     100 |     100 |                   
  ...ngsDialog.tsx |   68.27 |    71.66 |      75 |   68.27 | ...16-824,830-831 
  ...ionDialog.tsx |    92.3 |    96.15 |   33.33 |    92.3 | 60-63,68-75,164   
  ...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            
  ...tivityTab.tsx |    3.94 |      100 |       0 |    3.94 | 27-275            
  StatsDialog.tsx  |    8.85 |      100 |       0 |    8.85 | ...5,49-84,92-238 
  StatsDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...ciencyTab.tsx |    3.28 |      100 |       0 |    3.28 | 25-258            
  ...atmapView.tsx |    8.98 |      100 |       0 |    8.98 | 20-107            
  ...essionTab.tsx |    5.46 |      100 |       0 |    5.46 | 24-215            
  ...ineDialog.tsx |    93.5 |    85.18 |     100 |    93.5 | ...05,267,287-289 
  ...yTodoList.tsx |   96.33 |    88.23 |     100 |   96.33 | 137-140           
  ...nsDisplay.tsx |   90.47 |    75.86 |     100 |   90.47 | ...46,149,176-178 
  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            
  ...xitDialog.tsx |   80.36 |    43.47 |      60 |   80.36 | ...24-238,248-251 
  ...odeVisuals.ts |   91.42 |    64.28 |     100 |   91.42 | 15,21,24          
  ...s-helpers.tsx |      25 |      100 |       0 |      25 | ...3,86-89,94-102 
 ...nts/agent-view |    53.7 |    70.58 |   42.85 |    53.7 |                   
  ...atContent.tsx |    8.83 |      100 |       0 |    8.83 | 53-268,274-276    
  ...tChatView.tsx |   21.05 |      100 |       0 |   21.05 | 21-39             
  ...tComposer.tsx |   64.78 |    29.41 |   33.33 |   64.78 | ...51,269,277-279 
  AgentFooter.tsx  |   17.07 |      100 |       0 |   17.07 | 28-66             
  AgentHeader.tsx  |   15.38 |      100 |       0 |   15.38 | 27-64             
  AgentTabBar.tsx  |   87.39 |    62.85 |     100 |   87.39 | ...,85,98-106,124 
  ...oryAdapter.ts |     100 |    91.83 |     100 |     100 | 103,109-110,138   
  index.ts         |       0 |        0 |       0 |       0 | 1-12              
 ...mponents/arena |   45.59 |    70.53 |   60.86 |   45.59 |                   
  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 |    9.92 |      100 |       0 |    9.92 | 27-164            
  ...tusDialog.tsx |    5.63 |      100 |       0 |    5.63 | 33-75,80-288      
  ...topDialog.tsx |    6.17 |      100 |       0 |    6.17 | 33-213            
 ...ackground-view |   77.93 |       80 |   89.47 |   77.93 |                   
  ...sksDialog.tsx |   74.53 |       77 |    82.6 |   74.53 | ...1444,1506-1508 
  ...TasksPill.tsx |   67.03 |     86.2 |     100 |   67.03 | ...02-122,130-138 
  ...gentPanel.tsx |   97.43 |    85.39 |     100 |   97.43 | 121,436-440       
  ...Visibility.ts |     100 |      100 |     100 |     100 |                   
 ...nts/extensions |   84.58 |    78.18 |   83.33 |   84.58 |                   
  ...gerDialog.tsx |   82.46 |    77.77 |     100 |   82.46 | ...89,191-198,258 
  TabBar.tsx       |   97.29 |    88.88 |     100 |   97.29 | 33                
  index.ts         |       0 |        0 |       0 |       0 | 1-12              
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...tensions/steps |   46.87 |    84.74 |   58.82 |   46.87 |                   
  ...ctionStep.tsx |   95.12 |    92.85 |   85.71 |   95.12 | 84-86,89          
  ...etailStep.tsx |       0 |        0 |       0 |       0 | 1-145             
  ...nListStep.tsx |   75.54 |    88.09 |   66.66 |   75.54 | ...52,173,198-202 
  ...electStep.tsx |       0 |        0 |       0 |       0 | 1-77              
  ...nfirmStep.tsx |   17.39 |      100 |       0 |   17.39 | 28-71             
  index.ts         |       0 |        0 |       0 |       0 | 1-11              
 ...xtensions/tabs |   66.18 |    67.65 |   66.66 |   66.18 |                   
  DiscoverTab.tsx  |   57.21 |     63.2 |   55.55 |   57.21 | ...98,661-665,669 
  InstalledTab.tsx |   71.62 |    68.65 |   83.33 |   71.62 | ...67,772-773,810 
  SourcesTab.tsx   |   69.25 |     70.4 |   66.66 |   69.25 | ...16,535,607-619 
 ...tensions/views |   23.51 |    45.83 |    6.25 |   23.51 |                   
  ...tionsView.tsx |    6.02 |      100 |       0 |    6.02 | 52-65,68-368      
  ...tionsView.tsx |   45.89 |    45.83 |    9.09 |   45.89 | ...42-349,352-364 
  ...etailView.tsx |    9.56 |      100 |       0 |    9.56 | 40-67,70-158      
 ...mponents/hooks |   86.85 |    81.37 |   91.89 |   86.85 |                   
  ...rListBody.tsx |   95.29 |    85.18 |     100 |   95.29 | 95-98             
  ...etailStep.tsx |   75.32 |    71.42 |      60 |   75.32 | ...56-169,173-186 
  ...etailStep.tsx |     100 |      100 |     100 |     100 |                   
  ...rListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...entHeader.tsx |     100 |    85.71 |     100 |     100 | 47                
  ...rListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...etailStep.tsx |     100 |      100 |     100 |     100 |                   
  ...abledStep.tsx |     100 |      100 |     100 |     100 |                   
  ...sListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...entDialog.tsx |   72.29 |    70.49 |     100 |   72.29 | ...51,563-568,572 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-13              
  ...erGrouping.ts |     100 |      100 |     100 |     100 |                   
  sourceLabels.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...components/mcp |   20.21 |    91.83 |   76.92 |   20.21 |                   
  ...ealthPill.tsx |   68.42 |    85.71 |     100 |   68.42 | 40-46             
  ...entDialog.tsx |    3.32 |      100 |       0 |    3.32 | 46-793            
  ...valDialog.tsx |   15.06 |      100 |       0 |   15.06 | 40-109            
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-30              
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |      97 |       95 |     100 |      97 | 24,113-114        
 ...ents/mcp/steps |   40.02 |    40.86 |   43.75 |   40.02 |                   
  ...icateStep.tsx |    5.65 |      100 |       0 |    5.65 | 40-66,69-308      
  ...electStep.tsx |   10.95 |      100 |       0 |   10.95 | 16-88             
  ...etailStep.tsx |   61.65 |        8 |   33.33 |   61.65 | ...21-230,241-260 
  ...rListStep.tsx |   74.82 |    55.88 |     100 |   74.82 | ...65-170,181-185 
  ...etailStep.tsx |    10.3 |      100 |       0 |    10.3 | ...1,67-79,82-140 
  ToolListStep.tsx |   69.29 |       50 |     100 |   69.29 | ...23,126,135-144 
 ...nents/messages |   83.19 |    79.89 |   78.82 |   83.19 |                   
  ...ionDialog.tsx |   80.84 |     77.6 |    62.5 |   80.84 | ...98,516,534-536 
  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 |   81.14 |    75.55 |    92.3 |   81.14 | ...00-406,460-466 
  DiffRenderer.tsx |   93.19 |    86.17 |     100 |   93.19 | ...09,237-238,304 
  ...tsDisplay.tsx |   97.82 |    77.27 |     100 |   97.82 | 87,89             
  ...usMessage.tsx |   76.31 |     42.1 |   66.66 |   76.31 | ...99,101,124,155 
  ...tsDisplay.tsx |   95.18 |    88.05 |     100 |   95.18 | ...32,134,167-172 
  ...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 |   82.31 |    74.02 |   33.33 |   82.31 | ...69-471,478-480 
  ...upMessage.tsx |   82.63 |    92.85 |     100 |   82.63 | ...85-412,434-449 
  ToolMessage.tsx  |   88.19 |    74.49 |    92.3 |   88.19 | ...60-765,792-794 
 ...ponents/shared |   84.45 |    80.93 |    95.5 |   84.45 |                   
  ...ctionList.tsx |     100 |      100 |     100 |     100 |                   
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  EnumSelector.tsx |     100 |    96.42 |     100 |     100 | 58                
  MaxSizedBox.tsx  |   83.01 |    86.15 |   88.88 |   83.01 | ...12-513,618-619 
  MultiSelect.tsx  |   93.58 |       75 |     100 |   93.58 | ...43,199-201,211 
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  ...eSelector.tsx |     100 |       60 |     100 |     100 | 40-45             
  ...lableList.tsx |   76.25 |       80 |     100 |   76.25 | 44-58,65-68       
  StaticRender.tsx |   72.72 |      100 |     100 |   72.72 | 31-33             
  TextInput.tsx    |    80.8 |    67.24 |      80 |    80.8 | ...36-240,252-258 
  ...apsedTime.tsx |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |     100 |      100 |     100 |     100 |                   
  ...lizedList.tsx |   84.26 |    80.88 |      90 |   84.26 | ...68-696,743-765 
  text-buffer.ts   |   85.94 |    81.73 |   97.91 |   85.94 | ...2651,2749-2750 
  ...er-actions.ts |   73.93 |    67.22 |     100 |   73.93 | ...32-733,934-936 
 ...ponents/skills |    3.61 |      100 |       0 |    3.61 |                   
  ...gerDialog.tsx |    3.61 |      100 |       0 |    3.61 | ...90-148,151-694 
 ...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 |   21.51 |    59.52 |   27.27 |   21.51 |                   
  ...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 |   35.42 |    59.52 |     100 |   35.42 | ...20-432,437-439 
  ...iewerStep.tsx |   13.72 |      100 |       0 |   13.72 | 18-73             
  ...gerDialog.tsx |    6.74 |      100 |       0 |    6.74 | 35-341            
 ...mponents/views |   70.24 |    67.32 |    64.7 |   70.24 |                   
  ContextUsage.tsx |   70.88 |    63.88 |      80 |   70.88 | ...20-426,463-557 
  DoctorReport.tsx |     9.8 |      100 |       0 |     9.8 | 25-54,57-131      
  ...sionsList.tsx |   87.87 |    73.68 |     100 |   87.87 | 69-76             
  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.47 |    77.77 |   81.03 |   77.47 |                   
  ...ewContext.tsx |   64.83 |    88.88 |      50 |   64.83 | ...16-219,225-235 
  AppContext.tsx   |      80 |       50 |     100 |      80 | 19-20             
  ...ewContext.tsx |   92.45 |    62.79 |      50 |   92.45 | ...69-270,272-276 
  ...deContext.tsx |     100 |      100 |     100 |     100 |                   
  ...igContext.tsx |   81.81 |       50 |     100 |   81.81 | 15-16             
  ...ssContext.tsx |   81.67 |     81.6 |     100 |   81.67 | ...1199,1203-1205 
  ...owContext.tsx |   91.07 |    81.81 |     100 |   91.07 | 47-48,60-62       
  ...deContext.tsx |     100 |      100 |      50 |     100 |                   
  ...onContext.tsx |   43.26 |     62.5 |    62.5 |   43.26 | ...64-267,276-279 
  ...gsContext.tsx |     100 |      100 |     100 |     100 |                   
  ...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.88 |       50 |     100 |   88.88 | 134-135           
  ...teContext.tsx |   86.66 |       50 |     100 |   86.66 | 209-210           
  ...deContext.tsx |      80 |     87.5 |      75 |      80 | ...11-112,118-120 
 src/ui/daemon     |   90.65 |    73.61 |   95.45 |   90.65 |                   
  ...TuiAdapter.ts |   90.65 |    73.61 |   95.45 |   90.65 | ...44,762-763,849 
 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      |   82.65 |    81.22 |   87.45 |   82.65 |                   
  ...dProcessor.ts |   84.36 |    80.53 |     100 |   84.36 | ...44-445,451-456 
  keyToAnsi.ts     |    3.92 |      100 |       0 |    3.92 | 19-77             
  ...dProcessor.ts |   94.62 |    73.58 |     100 |   94.62 | ...86-287,292-293 
  ...dProcessor.ts |   84.56 |    63.15 |   81.25 |   84.56 | ...1024,1045-1049 
  ...amingState.ts |   12.22 |      100 |       0 |   12.22 | 54-157            
  ...agerDialog.ts |   88.23 |      100 |     100 |   88.23 | 20,24             
  ...dScrollbar.ts |     100 |      100 |     100 |     100 |                   
  ...ationFrame.ts |      52 |    63.63 |     100 |      52 | ...59,67-70,76-87 
  ...odeCommand.ts |   58.82 |      100 |     100 |   58.82 | 28,33-48          
  ...enaCommand.ts |      85 |      100 |     100 |      85 | 23-24,29          
  ...aInProcess.ts |   27.92 |       80 |      25 |   27.92 | ...69-170,173-175 
  ...Completion.ts |   91.79 |    86.88 |     100 |   91.79 | ...05-206,243-246 
  ...ifications.ts |   86.91 |    96.29 |     100 |   86.91 | 116-130           
  ...tIndicator.ts |   83.49 |    70.96 |     100 |   83.49 | ...60,168,170-178 
  ...waySummary.ts |   96.22 |    69.69 |     100 |   96.22 | 125-127,169       
  ...ndTaskView.ts |   93.84 |    74.46 |     100 |   93.84 | ...25-129,218,224 
  ...chedScroll.ts |     100 |      100 |     100 |     100 |                   
  ...ketedPaste.ts |    23.8 |      100 |       0 |    23.8 | 19-37             
  ...nchCommand.ts |   93.19 |    71.05 |     100 |   93.19 | ...37,183,256-259 
  ...ompletion.tsx |   95.36 |    82.81 |     100 |   95.36 | ...32-233,235-236 
  ...dMigration.ts |    92.1 |    88.88 |     100 |    92.1 | 42-44             
  useCompletion.ts |   94.11 |    89.65 |     100 |   94.11 | ...32-133,137-138 
  ...nitMessage.ts |     100 |      100 |     100 |     100 |                   
  ...extualTips.ts |   77.27 |       50 |     100 |   77.27 | ...2,75-79,93-101 
  ...eteCommand.ts |   78.53 |    88.57 |     100 |   78.53 | ...96-104,112-113 
  ...ialogClose.ts |    12.5 |      100 |     100 |    12.5 | 85-181            
  useDiffData.ts   |   11.62 |      100 |       0 |   11.62 | 44-87             
  ...oublePress.ts |   53.12 |       75 |     100 |   53.12 | 33-35,41-54       
  ...orSettings.ts |     100 |      100 |     100 |     100 |                   
  ...Completion.ts |   99.12 |    97.64 |     100 |   99.12 | 182-183           
  ...ionUpdates.ts |    93.5 |     92.3 |     100 |    93.5 | ...87-291,304-310 
  ...agerDialog.ts |   88.88 |      100 |     100 |   88.88 | 21,25             
  ...backDialog.ts |    63.9 |    76.47 |   66.66 |    63.9 | ...66-168,190-191 
  useFocus.ts      |     100 |      100 |     100 |     100 |                   
  ...olderTrust.ts |     100 |      100 |     100 |     100 |                   
  ...ggestions.tsx |   96.47 |    78.94 |     100 |   96.47 | 121,155-156       
  ...miniStream.ts |   82.76 |    79.44 |   94.44 |   82.76 | ...3036,3119-3127 
  ...BranchName.ts |   96.72 |       90 |     100 |   96.72 | 22-23             
  ...oryManager.ts |   97.94 |    98.24 |     100 |   97.94 | 139-142           
  ...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     |      16 |      100 |       0 |      16 | 15-45             
  useMCPHealth.ts  |   63.15 |       75 |      50 |   63.15 | 42-52,64-67       
  ...cpApproval.ts |   92.37 |    83.33 |     100 |   92.37 | ...00-103,115-116 
  useMcpDialog.ts  |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...moryDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...oryMonitor.ts |   83.14 |    78.57 |     100 |   83.14 | 54-63,74-79       
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...delCommand.ts |     100 |       75 |     100 |     100 | 22                
  ...ouseEvents.ts |   87.17 |    88.88 |   66.66 |   87.17 | 81-82,86-88       
  ...raseCycler.ts |   84.74 |    76.47 |     100 |   84.74 | ...49,52-53,69-71 
  ...rredEditor.ts |   58.33 |    22.22 |     100 |   58.33 | 23-27,29-33       
  ...derUpdates.ts |   86.61 |    77.96 |    90.9 |   86.61 | ...26,290-302,350 
  useQwenAuth.ts   |     100 |      100 |     100 |     100 |                   
  ...lScheduler.ts |   88.54 |    91.83 |     100 |   88.54 | ...73-278,381-391 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-7               
  ...tleRepaint.ts |     100 |      100 |     100 |     100 |                   
  ...umeCommand.ts |   93.47 |       68 |     100 |   93.47 | ...17,152,193-198 
  ...ompletion.tsx |   90.59 |    83.33 |     100 |   90.59 | ...01,104,137-140 
  ...ectionList.ts |   97.05 |    96.11 |     100 |   97.05 | ...90-191,245-248 
  ...sionPicker.ts |   92.87 |    90.35 |     100 |   92.87 | ...99-501,503-505 
  ...earchInput.ts |     100 |    97.29 |     100 |     100 | 82                
  ...ngsCommand.ts |   18.75 |      100 |       0 |   18.75 | 10-25             
  ...ellHistory.ts |   93.28 |    80.95 |     100 |   93.28 | ...96,153-154,164 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-73              
  ...agerDialog.ts |   88.23 |      100 |     100 |   88.23 | 20,24             
  ...Completion.ts |   82.73 |    85.41 |   94.73 |   82.73 | ...70-672,680-716 
  ...tateAndRef.ts |   95.45 |    66.66 |     100 |   95.45 | 25                
  ...tatsDialog.ts |     100 |      100 |     100 |     100 |                   
  useStatusLine.ts |    96.3 |    92.19 |     100 |    96.3 | ...77-380,466-473 
  ...eateDialog.ts |   88.23 |      100 |     100 |   88.23 | 14,18             
  ...mInProcess.ts |   27.35 |       80 |      25 |   27.35 | ...82-183,186-188 
  ...tification.ts |     100 |     87.5 |     100 |     100 | 50                
  ...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 |                   
  useTurnDiffs.ts  |   95.12 |    78.57 |     100 |   95.12 | 133-134,156-157   
  ...elcomeBack.ts |   87.36 |     90.9 |     100 |   87.36 | ...,94-96,114-115 
  ...reeSession.ts |   93.75 |    72.72 |     100 |   93.75 | 47-48,72          
  vim.ts           |      74 |    67.56 |   69.23 |      74 | ...1854-1861,1869 
 src/ui/layouts    |    90.9 |    90.62 |     100 |    90.9 |                   
  ...AppLayout.tsx |   90.72 |       90 |     100 |   90.72 | 57-59,101-106     
  ...AppLayout.tsx |   91.17 |    91.66 |     100 |   91.17 | 70-75             
 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 |    6.66 |     100 |                   
  ...eractiveUi.ts |     100 |      100 |    6.66 |     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.39 |    72.83 |     100 |   98.39 |                   
  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   |   97.91 |       92 |     100 |   97.91 | ...51-352,354-355 
  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.63 |    83.09 |   92.94 |   83.63 |                   
  ...Colorizer.tsx |   80.42 |    85.41 |     100 |   80.42 | ...00-201,298-324 
  ...nRenderer.tsx |   68.83 |    70.14 |      50 |   68.83 | ...52-254,274-293 
  ...wnDisplay.tsx |   86.01 |    87.66 |     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 
  ...odeDisplay.ts |   96.55 |     90.9 |     100 |   96.55 | 34                
  asciiCharts.ts   |   96.77 |    87.62 |     100 |   96.77 | 173-180,281       
  ...dWorkUtils.ts |     100 |      100 |     100 |     100 |                   
  ...boardUtils.ts |   51.92 |    72.72 |   91.66 |   51.92 | ...21,624-633,636 
  commandUtils.ts  |      96 |    88.77 |     100 |      96 | ...72,174-175,302 
  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.4 |    98.38 |     100 |    95.4 | 123-126           
  gradientUtils.ts |     100 |      100 |     100 |     100 |                   
  highlight.ts     |     100 |      100 |     100 |     100 |                   
  ...oryMapping.ts |     100 |    96.77 |     100 |     100 | 43                
  historyUtils.ts  |   94.11 |       94 |     100 |   94.11 | 94-97             
  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 |                   
  ...ightLoader.ts |     100 |    89.47 |     100 |     100 | 81,110            
  ...nUtilities.ts |   90.21 |    85.71 |     100 |   90.21 | ...,91-95,107-108 
  ...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                
  mouse.ts         |   90.71 |    73.33 |   88.88 |   90.71 | ...40-143,200-201 
  osc8.ts          |   94.73 |    87.75 |     100 |   94.73 | ...49,434,438-439 
  ...mConstants.ts |     100 |      100 |     100 |     100 |                   
  restoreGoal.ts   |   99.02 |    97.56 |     100 |   99.02 | 106               
  ...storyUtils.ts |   66.66 |    75.96 |   93.33 |   66.66 | ...37-459,580-581 
  ...ickerUtils.ts |     100 |      100 |     100 |     100 |                   
  ...ataService.ts |   93.17 |     79.1 |     100 |   93.17 | ...14,227,254-256 
  ...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.61 |    94.84 |   92.85 |   97.61 | ...50-251,386-387 
  todoSnapshot.ts  |   89.33 |    93.47 |     100 |   89.33 | ...,66-78,180-181 
  updateCheck.ts   |     100 |    80.95 |     100 |     100 | 30-42             
 ...i/utils/export |      57 |     40.8 |   79.41 |      57 |                   
  collect.ts       |   55.92 |    50.58 |   86.36 |   55.92 | ...25-640,642-647 
  index.ts         |     100 |      100 |     100 |     100 |                   
  normalize.ts     |   58.11 |    20.51 |      80 |   58.11 | ...13-314,328-363 
  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.39 |    89.79 |   90.74 |   73.39 |                   
  acpModelUtils.ts |     100 |      100 |     100 |     100 |                   
  apiPreconnect.ts |   96.72 |    97.14 |     100 |   96.72 | 165-168           
  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   |   90.51 |    91.89 |     100 |   90.51 | 67-76,116         
  ...Calculator.ts |     100 |      100 |     100 |     100 |                   
  cpuProfiler.ts   |   70.38 |    71.83 |   88.88 |   70.38 | ...27,430-431,438 
  deepMerge.ts     |     100 |       90 |     100 |     100 | 41-43,49          
  ...ScopeUtils.ts |   97.56 |    88.88 |     100 |   97.56 | 67                
  doctorChecks.ts  |   70.31 |    74.57 |     100 |   70.31 | ...95-301,325-341 
  ...putCapture.ts |   90.65 |    86.17 |     100 |   90.65 | ...72,370,372-373 
  ...arResolver.ts |   97.14 |    96.42 |     100 |   97.14 | 125-126           
  errors.ts        |   90.85 |    96.36 |    92.3 |   90.85 | 69-70,298-310     
  events.ts        |     100 |      100 |     100 |     100 |                   
  gitUtils.ts      |   91.22 |    82.35 |     100 |   91.22 | ...,94-97,140-143 
  ...AutoUpdate.ts |    92.2 |    95.23 |   88.88 |    92.2 | 130-141           
  ...tyWarnings.ts |     100 |      100 |     100 |     100 |                   
  ...lationInfo.ts |    98.4 |    96.34 |     100 |    98.4 | 241,258-259,304   
  languageUtils.ts |   98.47 |    97.67 |     100 |   98.47 | 153-154           
  math.ts          |       0 |        0 |       0 |       0 | 1-15              
  ...iagnostics.ts |   94.57 |    83.01 |   88.88 |   94.57 | ...05,311,315-317 
  ...serMessage.ts |     100 |      100 |     100 |     100 |                   
  ...onfigUtils.ts |     100 |    98.92 |     100 |     100 | 218               
  ...iveHelpers.ts |   96.93 |    93.84 |     100 |   96.93 | ...15-416,514,527 
  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     |   93.67 |    94.11 |   85.71 |   93.67 | 79-83             
  relaunch.ts      |   93.22 |    81.25 |     100 |   93.22 | 65-67,80          
  resolvePath.ts   |     100 |      100 |     100 |     100 |                   
  runBudget.ts     |   99.35 |    96.77 |     100 |   99.35 | 119               
  sandbox-path.ts  |     100 |      100 |     100 |     100 |                   
  sandbox.ts       |       0 |        0 |       0 |       0 | 1-1042            
  ...xImageName.ts |     100 |    77.77 |     100 |     100 | 10,18             
  sandboxMounts.ts |     100 |      100 |     100 |     100 |                   
  sessionPaths.ts  |   90.84 |    90.56 |     100 |   90.84 | ...81-182,185-186 
  settingsUtils.ts |   82.51 |    91.85 |   89.74 |   82.51 | ...76-694,701-709 
  spawnWrapper.ts  |     100 |      100 |     100 |     100 |                   
  ...ate-verify.ts |     100 |      100 |     100 |     100 |                   
  ...one-update.ts |   26.82 |    73.77 |   43.47 |   26.82 | ...36-837,840-859 
  ...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    |   95.12 |    89.06 |     100 |   95.12 | ...43-244,249-253 
  ...InfoFields.ts |    87.5 |    65.85 |     100 |    87.5 | ...24-125,146-147 
  ...alSequence.ts |     100 |    97.61 |     100 |     100 | 60                
  ...iffPreview.ts |   94.11 |    83.33 |     100 |   94.11 | 13                
  ...entEmitter.ts |     100 |      100 |     100 |     100 |                   
  ...ansionHook.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                
  ...ingHandler.ts |     100 |      100 |     100 |     100 |                   
  windowTitle.ts   |   95.45 |    93.33 |     100 |   95.45 | 54-55             
  ...WithBackup.ts |   65.04 |    77.77 |     100 |   65.04 | 97,112,133-172    
 ...s/housekeeping |   90.15 |     89.7 |   94.11 |   90.15 |                   
  cleanup.ts       |   94.33 |       95 |     100 |   94.33 | 60-62             
  ...eractionAt.ts |     100 |      100 |     100 |     100 |                   
  scheduler.ts     |   89.71 |    88.23 |   85.71 |   89.71 | 51-55,66,116-120  
  throttledOnce.ts |   86.66 |    85.18 |     100 |   86.66 | ...99,105,137-138 
-------------------|---------|----------|---------|---------|-------------------
Core Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   82.94 |    84.35 |   84.61 |   82.94 |                   
 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        |   89.14 |    80.96 |   94.21 |   89.14 |                   
  ...transcript.ts |   92.25 |    85.71 |     100 |   92.25 | ...01,320-321,452 
  ...ent-resume.ts |   83.08 |    69.86 |   78.12 |   83.08 | ...1120-1124,1127 
  ...ound-tasks.ts |   95.07 |    88.12 |     100 |   95.07 | ...1151,1171-1174 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...n-registry.ts |   95.36 |    88.31 |     100 |   95.36 | ...64-365,437-441 
 src/agents/arena  |   76.54 |    66.87 |   78.72 |   76.54 |                   
  ...gentClient.ts |   79.47 |    88.88 |   81.81 |   79.47 | ...68-183,189-204 
  ArenaManager.ts  |   75.37 |    63.37 |   78.26 |   75.37 | ...1860,1866-1867 
  arena-events.ts  |   64.44 |      100 |      50 |   64.44 | ...71-175,178-183 
  diff-summary.ts  |    87.5 |    72.34 |     100 |    87.5 | ...32-133,137-138 
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...gents/backends |   76.43 |    86.23 |   73.04 |   76.43 |                   
  ITermBackend.ts  |   97.97 |    93.93 |     100 |   97.97 | ...78-180,255,307 
  ...essBackend.ts |   91.98 |     90.9 |   86.66 |   91.98 | ...95,250-270,329 
  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 |   86.96 |    85.21 |   78.97 |   86.96 |                   
  agent-context.ts |     100 |      100 |     100 |     100 |                   
  agent-core.ts    |   78.78 |    74.15 |   65.95 |   78.78 | ...1787,1814-1861 
  agent-events.ts  |     100 |      100 |     100 |     100 |                   
  ...t-headless.ts |   87.93 |    79.06 |   63.63 |   87.93 | ...00-401,404-405 
  ...nteractive.ts |   80.55 |    81.35 |   74.07 |   80.55 | ...79,481,483,486 
  ...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 |                   
  ...low-budget.ts |     100 |      100 |     100 |     100 |                   
  ...chestrator.ts |   91.65 |    89.82 |   80.64 |   91.65 | ...1497,1546-1549 
  ...ow-prompts.ts |     100 |      100 |     100 |     100 |                   
  ...ow-sandbox.ts |   96.85 |    94.54 |     100 |   96.85 | ...24-325,330-331 
 src/agents/tasks  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/agents/team   |   80.31 |    83.19 |    86.5 |   80.31 |                   
  TeamManager.ts   |   67.11 |    76.25 |   74.41 |   67.11 | ...1433,1456-1457 
  identity.ts      |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...sionBridge.ts |     100 |      100 |     100 |     100 |                   
  mailbox.ts       |   94.76 |    86.36 |   92.85 |   94.76 | 86-87,348-354     
  ...ptAddendum.ts |     100 |      100 |     100 |     100 |                   
  tasks.ts         |   88.85 |    82.47 |   96.29 |   88.85 | ...-990,1034-1035 
  team-events.ts   |   60.52 |      100 |      50 |   60.52 | ...37-141,148-152 
  teamHelpers.ts   |   92.02 |    94.91 |   95.23 |   92.02 | ...31-332,368-378 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...eam/test-utils |   94.39 |    93.38 |   98.21 |   94.39 |                   
  ...on-harness.ts |   96.49 |    77.77 |     100 |   96.49 | 128-129,141-142   
  fake-agent.ts    |   98.49 |    95.08 |     100 |   98.49 | 201-203           
  fake-backend.ts  |   86.46 |    97.61 |   95.83 |   86.46 | 124-146           
 src/config        |   78.91 |    83.58 |   64.68 |   78.91 |                   
  ...xtDefaults.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |   77.34 |    82.75 |   61.15 |   77.34 | ...4990,4995-4996 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  models.ts        |     100 |      100 |     100 |     100 |                   
  storage.ts       |   94.24 |    91.13 |   88.09 |   94.24 | ...68-369,372-373 
 ...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          |   88.43 |    84.06 |   91.95 |   88.43 |                   
  baseLlmClient.ts |   81.74 |    77.52 |   77.77 |   81.74 | ...24,526-536,545 
  client.ts        |   87.82 |    81.58 |      90 |   87.82 | ...2628,2723-2724 
  ...tGenerator.ts |   84.86 |    69.23 |     100 |   84.86 | ...90,392,399-402 
  ...lScheduler.ts |   87.63 |    82.06 |   95.94 |   87.63 | ...4165,4193-4204 
  geminiChat.ts    |   89.04 |    87.24 |      95 |   89.04 | ...3253,3320-3321 
  geminiRequest.ts |     100 |      100 |     100 |     100 |                   
  ...MediaLimit.ts |     100 |    95.83 |     100 |     100 | 96                
  ...htProtocol.ts |    9.09 |      100 |       0 |    9.09 | ...9,62-66,69-110 
  logger.ts        |   87.41 |    87.02 |     100 |   87.41 | ...64-568,614-628 
  ...tyDefaults.ts |     100 |      100 |     100 |     100 |                   
  ...olExecutor.ts |   92.59 |       75 |      50 |   92.59 | 41-42             
  ...on-helpers.ts |   86.48 |    72.22 |     100 |   86.48 | ...97-198,212-221 
  ...issionFlow.ts |   98.78 |       96 |     100 |   98.78 | 93                
  prompts.ts       |   88.93 |    87.87 |   72.72 |   88.93 | ...-910,1113-1114 
  ...port-retry.ts |     100 |      100 |     100 |     100 |                   
  tokenLimits.ts   |     100 |    89.47 |     100 |     100 | 51-52             
  ...allIdUtils.ts |   98.23 |     92.1 |     100 |   98.23 | 36,45             
  ...okTriggers.ts |   99.43 |     91.5 |     100 |   99.43 | 175,186           
  turn.ts          |   96.81 |    88.13 |     100 |   96.81 | ...60,473-474,520 
 ...ntentGenerator |   94.88 |    82.07 |      94 |   94.88 |                   
  ...tGenerator.ts |   96.29 |    83.18 |   92.85 |   96.29 | ...1,971,999-1001 
  converter.ts     |   94.51 |    80.72 |     100 |   94.51 | ...06-607,617,823 
  index.ts         |       0 |        0 |       0 |       0 | 1-21              
  usage.ts         |     100 |      100 |     100 |     100 |                   
 ...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 |   94.24 |    84.03 |   91.17 |   94.24 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tGenerator.ts |   94.11 |    82.57 |   90.62 |   94.11 | ...1028-1029,1057 
  ...tDetection.ts |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   87.81 |    84.42 |   93.67 |   87.81 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  converter.ts     |   87.57 |    82.32 |   96.15 |   87.57 | ...1508,1677-1692 
  errorHandler.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |   54.54 |    68.75 |      50 |   54.54 | ...79,87-91,95-99 
  ...tGenerator.ts |    66.4 |    70.58 |   88.88 |    66.4 | ...51-157,168-169 
  pipeline.ts      |   94.44 |    86.71 |     100 |   94.44 | ...38-539,547,615 
  ...ureContext.ts |     100 |      100 |     100 |     100 |                   
  ...ingOptions.ts |       0 |        0 |       0 |       0 | 1                 
  ...CallParser.ts |    90.2 |    87.65 |     100 |    90.2 | ...39-343,373-374 
  ...kingParser.ts |     100 |    96.87 |     100 |     100 | 42                
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...rator/provider |   96.67 |    88.94 |   96.07 |   96.67 |                   
  dashscope.ts     |   97.37 |    91.39 |   93.33 |   97.37 | ...90-291,369-370 
  deepseek.ts      |   94.91 |    89.36 |     100 |   94.91 | ...31-132,145-146 
  default.ts       |   95.79 |    89.65 |   88.88 |   95.79 | 122-123,193-195   
  index.ts         |     100 |      100 |     100 |     100 |                   
  mimo.ts          |   94.11 |    66.66 |     100 |   94.11 | 29,52-53          
  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 |                   
  utils.ts         |     100 |      100 |     100 |     100 |                   
 src/extension     |   66.78 |    81.05 |   77.95 |   66.78 |                   
  ...-converter.ts |   75.65 |    67.18 |     100 |   75.65 | ...1086,1131-1132 
  corruptFile.ts   |     100 |       50 |     100 |     100 | 40-45             
  ...ionManager.ts |   45.13 |    82.31 |      50 |   45.13 | ...1615,1625-1644 
  ...references.ts |     100 |    89.58 |     100 |     100 | ...05,129,197,200 
  ...onSettings.ts |   92.65 |    91.66 |     100 |   92.65 | ...28-232,312-313 
  ...-converter.ts |   63.25 |    86.66 |   71.42 |   63.25 | ...98,202,214-248 
  github.ts        |   46.41 |     87.3 |   63.63 |   46.41 | ...66-372,411-464 
  i18n.ts          |   78.26 |    95.83 |      50 |   78.26 | 104-110,116-123   
  index.ts         |     100 |      100 |     100 |     100 |                   
  marketplace.ts   |   83.39 |    81.53 |     100 |   83.39 | ...17-322,326-332 
  npm.ts           |   59.01 |    71.69 |    87.5 |   59.01 | ...23-425,432-436 
  override.ts      |   94.11 |    88.88 |     100 |   94.11 | 63-64,81-82       
  redaction.ts     |     100 |      100 |     100 |     100 |                   
  settings.ts      |   66.26 |      100 |      50 |   66.26 | 81-107,141-146    
  ...ceRegistry.ts |   93.93 |    82.95 |     100 |   93.93 | ...34-340,361-362 
  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      |   76.26 |    73.59 |    90.9 |   76.26 |                   
  followupState.ts |   98.44 |    95.74 |     100 |   98.44 | 236-237           
  index.ts         |     100 |      100 |     100 |     100 |                   
  overlayFs.ts     |   96.29 |    88.88 |     100 |   96.29 | 78,108,122        
  speculation.ts   |   63.01 |    40.29 |   71.42 |   63.01 | ...73-574,577-582 
  ...onToolGate.ts |     100 |    96.55 |     100 |     100 | 95                
  ...nGenerator.ts |   70.35 |     75.4 |   83.33 |   70.35 | ...83-247,326-328 
 src/generated     |       0 |        0 |       0 |       0 |                   
  git-commit.ts    |       0 |        0 |       0 |       0 | 1-10              
 src/goals         |   89.57 |    83.57 |   94.44 |   89.57 |                   
  ...eGoalStore.ts |    85.1 |    95.45 |   84.61 |    85.1 | ...63-166,174-182 
  goalHook.ts      |   97.26 |    91.66 |     100 |   97.26 | 100-105           
  goalJudge.ts     |   84.33 |    74.28 |     100 |   84.33 | ...57-358,366-368 
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/hooks         |   86.93 |    85.44 |   88.01 |   86.93 |                   
  ...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.35 |    90.69 |     100 |   96.35 | ...00-301,382,384 
  ...entHandler.ts |   95.32 |    85.05 |   94.11 |   95.32 | ...71,928-929,939 
  hookPlanner.ts   |   86.29 |    83.33 |   85.71 |   86.29 | ...15-219,226-237 
  hookRegistry.ts  |   91.48 |    84.61 |     100 |   91.48 | ...97,416,420,424 
  hookRunner.ts    |   62.42 |    72.04 |   66.66 |   62.42 | ...64-765,774-775 
  hookSystem.ts    |      87 |      100 |   68.88 |      87 | ...15-716,722-723 
  ...HookRunner.ts |   75.51 |     61.9 |      80 |   75.51 | ...05-406,424-425 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...edCallback.ts |     100 |      100 |     100 |     100 |                   
  ...HookRunner.ts |   96.37 |     90.9 |      90 |   96.37 | 342-350,424-425   
  ...SkillHooks.ts |   78.75 |       75 |   66.66 |   78.75 | 62-66,137-152     
  ...oksManager.ts |   96.66 |    91.66 |     100 |   96.66 | ...90,209-210,223 
  ssrfGuard.ts     |   77.22 |    85.36 |     100 |   77.22 | ...57,261-267,273 
  stopHookCap.ts   |     100 |      100 |     100 |     100 |                   
  trustedHooks.ts  |      90 |    52.63 |     100 |      90 | ...53,66-67,97-98 
  types.ts         |   92.83 |       94 |    87.5 |   92.83 | ...87-488,573-577 
  urlValidator.ts  |     100 |      100 |     100 |     100 |                   
 src/ide           |   75.55 |    83.52 |   78.33 |   75.55 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  detect-ide.ts    |     100 |      100 |     100 |     100 |                   
  ide-client.ts    |   66.14 |    81.75 |   66.66 |   66.14 | ...3-964,993-1001 
  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           |   42.42 |     51.9 |   52.14 |   42.42 |                   
  ...nfigLoader.ts |   70.27 |    35.89 |   94.73 |   70.27 | ...20-422,426-432 
  ...ionFactory.ts |   42.81 |    73.07 |      50 |   42.81 | ...76-427,433-450 
  ...Normalizer.ts |   23.09 |    13.72 |   30.43 |   23.09 | ...04-905,909-924 
  ...verManager.ts |   25.31 |    62.06 |   41.66 |   25.31 | ...85-704,710-740 
  ...eLspClient.ts |   32.77 |       80 |   17.64 |   32.77 | ...84-288,294-295 
  ...LspService.ts |   51.85 |    65.98 |   68.57 |   51.85 | ...1339,1399-1409 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mcp           |   82.17 |     77.5 |   77.96 |   82.17 |                   
  configHash.ts    |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...h-provider.ts |   86.95 |      100 |   33.33 |   86.95 | ...,93,97,101-102 
  ...h-provider.ts |   79.52 |    58.06 |     100 |   79.52 | ...33-940,947-949 
  ...en-storage.ts |   98.64 |    97.77 |     100 |   98.64 | 88-89             
  oauth-utils.ts   |   73.61 |    85.48 |    92.3 |   73.61 | ...46-366,392-421 
  ...n-provider.ts |   89.83 |       96 |   45.45 |   89.83 | ...43,147,151-152 
 .../token-storage |   82.12 |    88.19 |   89.28 |   82.12 |                   
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   87.08 |    87.27 |   95.23 |   87.08 | ...00-201,214-215 
  ...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        |   74.29 |    78.31 |   72.53 |   74.29 |                   
  const.ts         |   94.28 |     92.3 |     100 |   94.28 | 66-67             
  dream.ts         |      66 |    73.33 |      50 |      66 | 51,108-149        
  ...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       |   91.36 |    72.41 |     100 |   91.36 | ...99,118-121,189 
  ...entPlanner.ts |   67.59 |     73.8 |      50 |   67.59 | ...31,240-243,415 
  ...ionPlanner.ts |       0 |        0 |       0 |       0 | 1                 
  forget.ts        |   46.21 |    61.53 |   44.44 |   46.21 | ...06,213,216-348 
  indexer.ts       |    86.3 |       50 |     100 |    86.3 | ...56,62-63,75-76 
  manager.ts       |   78.48 |    83.23 |   76.19 |   78.48 | ...1363,1376-1378 
  memoryAge.ts     |   90.47 |       80 |     100 |   90.47 | 50-51             
  paths.ts         |   79.06 |    95.12 |     100 |   79.06 | 32-33,49-86       
  prompt.ts        |   94.87 |    78.57 |     100 |   94.87 | ...63,166,304-305 
  recall.ts        |   82.06 |       75 |    90.9 |   82.06 | ...59-364,395-406 
  ...ceSelector.ts |    93.1 |    81.81 |     100 |    93.1 | ...25,127-128,136 
  scan.ts          |   92.92 |    73.91 |     100 |   92.92 | ...51-52,62,90-91 
  ...entPlanner.ts |   58.33 |    66.66 |   56.25 |   58.33 | ...61-282,358-403 
  status.ts        |   10.52 |      100 |       0 |   10.52 | 41-98             
  store.ts         |   93.33 |    81.25 |     100 |   93.33 | ...,94-95,119-120 
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...ontextFile.ts |   79.38 |    81.03 |   81.81 |   79.38 | ...58-272,286-291 
 src/mocks         |       0 |        0 |       0 |       0 |                   
  msw.ts           |       0 |        0 |       0 |       0 | 1-9               
 src/models        |   90.02 |     87.5 |   88.15 |   90.02 |                   
  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 |    47.82 |   84.61 |   74.22 | ...,67-74,106-117 
  ...igResolver.ts |   98.66 |    92.85 |     100 |   98.66 | 162,324,330       
  modelRegistry.ts |     100 |    98.63 |     100 |     100 | 229               
  modelsConfig.ts  |   86.33 |    85.51 |   82.92 |   86.33 | ...1333,1362-1363 
  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   |   82.68 |    92.01 |   68.46 |   82.68 |                   
  autoMode.ts      |   97.84 |    94.27 |     100 |   97.84 | 523-524,545-552   
  ...transcript.ts |      98 |       84 |     100 |      98 | 200-201           
  classifier.ts    |      94 |    94.44 |     100 |      94 | 158-165,385-389   
  ...erousRules.ts |     100 |    89.36 |     100 |     100 | 110,133,147,175   
  ...alTracking.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...on-manager.ts |   84.89 |     89.1 |      80 |   84.89 | ...1025,1131-1135 
  rule-parser.ts   |    97.4 |    93.82 |     100 |    97.4 | ...-884,1033-1035 
  ...-semantics.ts |   70.36 |    91.07 |   46.66 |   70.36 | ...2237,2300-2303 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...sifier-prompts |   99.04 |    95.23 |     100 |   99.04 |                   
  system-prompt.ts |   99.04 |    95.23 |     100 |   99.04 | 219               
 src/plan-gate     |    75.9 |    91.42 |      80 |    75.9 |                   
  ...viewAgents.ts |   52.28 |    88.46 |   66.66 |   52.28 | ...24-220,242-243 
  ...provalGate.ts |   92.47 |    92.85 |   85.71 |   92.47 | ...86-187,268-274 
  state.ts         |     100 |      100 |     100 |     100 |                   
  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/providers     |   84.87 |    74.25 |   71.42 |   84.87 |                   
  all-providers.ts |      68 |      100 |       0 |      68 | 68-69,73-79,83-89 
  index.ts         |     100 |      100 |     100 |     100 |                   
  install.ts       |   98.93 |    87.71 |     100 |   98.93 | 286-287           
  ...der-config.ts |   79.17 |    67.28 |   78.94 |   79.17 | ...23-424,431-440 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...viders/presets |   97.28 |    86.36 |      50 |   97.28 |                   
  ...oding-plan.ts |   87.34 |      100 |       0 |   87.34 | 82-84,87-89,91-94 
  ...a-standard.ts |     100 |      100 |     100 |     100 |                   
  ...token-plan.ts |     100 |      100 |     100 |     100 |                   
  ...m-provider.ts |   97.05 |    81.25 |      75 |   97.05 | 118-119           
  deepseek.ts      |     100 |      100 |     100 |     100 |                   
  idealab.ts       |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  openrouter.ts    |     100 |      100 |     100 |     100 |                   
  zai.ts           |     100 |      100 |     100 |     100 |                   
 src/qwen          |    85.3 |    78.57 |   95.89 |    85.3 |                   
  ...tGenerator.ts |   98.64 |    98.18 |     100 |   98.64 | 105-106           
  qwenOAuth2.ts    |   82.55 |    73.24 |   90.62 |   82.55 | ...1183-1199,1229 
  ...kenManager.ts |   85.36 |    76.61 |     100 |   85.36 | ...52-757,778-783 
 src/services      |   87.92 |       85 |   93.85 |   87.92 |                   
  ...ionTrailer.ts |     100 |      100 |     100 |     100 |                   
  ...llRegistry.ts |   97.35 |    85.34 |     100 |   97.35 | ...94,117,417-418 
  ...ionService.ts |   96.46 |    94.44 |     100 |   96.46 | ...35,651,780-788 
  ...ingService.ts |   84.06 |    82.35 |   81.39 |   84.06 | ...1459,1474-1475 
  ...ttribution.ts |   91.73 |    87.71 |      90 |   91.73 | ...80-685,826-827 
  ...utSlimming.ts |     100 |    97.43 |     100 |     100 | 215,268           
  cronScheduler.ts |   95.09 |    90.18 |     100 |   95.09 | ...-940,1239-1240 
  cronTasksFile.ts |   95.03 |    89.47 |     100 |   95.03 | ...44-147,172-173 
  cronTasksLock.ts |   94.44 |    89.47 |     100 |   94.44 | ...02-103,132-133 
  ...eryService.ts |   80.43 |    95.45 |      75 |   80.43 | ...19-134,140-141 
  ...oryService.ts |   88.17 |    79.02 |    92.3 |   88.17 | ...1303,1344-1347 
  fileReadCache.ts |     100 |      100 |     100 |     100 |                   
  ...temService.ts |   91.27 |    82.69 |    90.9 |   91.27 | ...94,196,294-301 
  ...ratedFiles.ts |      96 |    88.23 |     100 |      96 | 119-120,146-147   
  gitInit.ts       |     100 |      100 |     100 |     100 |                   
  ...reeService.ts |    69.4 |    68.82 |   93.33 |    69.4 | ...2064,2092-2093 
  ...ionService.ts |   98.65 |    97.81 |     100 |   98.65 | ...57-458,505-506 
  ...ticsDumper.ts |   98.37 |    95.23 |     100 |   98.37 | 185-186           
  ...ureMonitor.ts |   96.06 |    91.48 |   96.96 |   96.06 | ...49,850,864-866 
  ...orRegistry.ts |   97.26 |    91.22 |     100 |   97.26 | ...50-451,605-606 
  ...ttachments.ts |   97.24 |    90.39 |     100 |   97.24 | ...08,646,661-662 
  sessionRecap.ts  |     9.7 |      100 |       0 |     9.7 | 42-172            
  ...ionService.ts |   87.31 |    79.37 |   94.44 |   87.31 | ...1483,1553-1573 
  sessionTitle.ts  |   93.87 |    71.15 |     100 |   93.87 | ...32-235,266-267 
  ...ionService.ts |   82.58 |    78.08 |   90.62 |   82.58 | ...2173,2179-2184 
  ...pInhibitor.ts |   97.34 |    92.68 |     100 |   97.34 | ...28,167,361-362 
  ...Estimation.ts |     100 |    86.66 |     100 |     100 | 96-97             
  ...ageService.ts |   97.76 |    91.59 |   93.75 |   97.76 | ...61-262,366,567 
  ...UseSummary.ts |   94.63 |    88.46 |     100 |   94.63 | ...62-164,214-215 
  ...oryService.ts |   89.03 |    65.38 |     100 |   89.03 | ...23-325,330-331 
  ...reeCleanup.ts |   14.56 |      100 |   33.33 |   14.56 | 58-185            
  ...ionService.ts |   87.98 |    86.72 |     100 |   87.98 | ...38-439,455-456 
 ...icrocompaction |   98.88 |    93.56 |     100 |   98.88 |                   
  microcompact.ts  |   98.88 |    93.56 |     100 |   98.88 | ...24,466,470,621 
 src/skills        |   88.14 |    86.62 |      90 |   88.14 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...activation.ts |     100 |     93.1 |     100 |     100 | 93,112            
  skill-load.ts    |   94.84 |     87.5 |     100 |   94.84 | ...03,223,235-237 
  skill-manager.ts |   83.39 |    81.42 |   82.35 |   83.39 | ...1199,1206-1210 
  skill-paths.ts   |   89.15 |    86.36 |     100 |   89.15 | ...00-101,106-107 
  symlinkScope.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |   97.91 |       98 |     100 |   97.91 | 277-278           
 src/subagents     |   85.84 |    85.55 |   94.33 |   85.84 |                   
  ...ter-schema.ts |     100 |    98.07 |     100 |     100 | 99                
  ...tin-agents.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nt-manager.ts |    81.2 |    79.93 |   91.17 |    81.2 | ...1432,1509-1510 
  types.ts         |     100 |      100 |     100 |     100 |                   
  validation.ts    |   92.46 |    95.18 |     100 |   92.46 | 47-52,63-68,71-76 
 src/telemetry     |   78.72 |     87.6 |    80.6 |   78.72 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...on-metrics.ts |   98.96 |    79.48 |     100 |   98.96 | 169,183           
  ...on-tracing.ts |   74.55 |    73.21 |   70.58 |   74.55 | ...95,336-338,354 
  ...attributes.ts |   98.13 |       88 |     100 |   98.13 | 185-187           
  ...-exporters.ts |   65.78 |    83.33 |   55.55 |   65.78 | ...04-105,108-109 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-111             
  ...-processor.ts |   99.09 |    95.61 |      95 |   99.09 | 141,365-366       
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-128             
  loggers.ts       |    54.1 |    66.26 |   60.86 |    54.1 | ...1257,1274-1294 
  metrics.ts       |   75.31 |    80.85 |   77.19 |   75.31 | ...1021,1024-1035 
  ...attributes.ts |     100 |      100 |     100 |     100 |                   
  ...ime-config.ts |       0 |        0 |       0 |       0 | 1                 
  sanitize.ts      |      80 |    83.33 |     100 |      80 | 35-36,41-42       
  sdk.ts           |   86.75 |     88.4 |   66.66 |   86.75 | ...17-621,659-681 
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...on-tracing.ts |   90.04 |    88.07 |   96.55 |   90.04 | ...1504,1535-1538 
  ...etry-utils.ts |     100 |      100 |     100 |     100 |                   
  ...l-decision.ts |     100 |      100 |     100 |     100 |                   
  trace-context.ts |     100 |      100 |     100 |     100 |                   
  ...e-id-utils.ts |     100 |      100 |     100 |     100 |                   
  tracer.ts        |   98.56 |    88.63 |     100 |   98.56 | 52,101            
  types.ts         |   79.36 |    93.91 |   84.21 |   79.36 | ...1250,1253-1282 
  uiTelemetry.ts   |      92 |    95.34 |   80.95 |      92 | ...00,206-216,244 
 ...ry/qwen-logger |   68.17 |     80.2 |   65.51 |   68.17 |                   
  event-types.ts   |       0 |        0 |       0 |       0 |                   
  qwen-logger.ts   |   68.17 |       80 |   64.91 |   68.17 | ...1077,1115-1116 
 src/test-utils    |   93.44 |    96.15 |   77.77 |   93.44 |                   
  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.71 |    97.36 |   74.19 |   91.71 | ...54,218-219,232 
  ...aceContext.ts |     100 |      100 |     100 |     100 |                   
 src/tools         |   80.47 |    82.33 |   86.68 |   80.47 |                   
  ...erQuestion.ts |   90.03 |    79.36 |   91.66 |   90.03 | ...99-400,407-408 
  cron-create.ts   |   88.18 |    93.33 |    62.5 |   88.18 | ...,45-46,177-185 
  cron-delete.ts   |   97.56 |      100 |   83.33 |   97.56 | 31-32             
  cron-list.ts     |   98.16 |    93.75 |    87.5 |   98.16 | 50-51             
  diffOptions.ts   |     100 |      100 |     100 |     100 |                   
  edit.ts          |   80.82 |    83.48 |      75 |   80.82 | ...08-709,819-869 
  ...r-worktree.ts |   83.14 |    67.56 |    87.5 |   83.14 | ...84-187,278-279 
  enterPlanMode.ts |   80.73 |    66.66 |   85.71 |   80.73 | ...,74-79,107-121 
  exit-worktree.ts |   83.29 |    83.65 |   94.44 |   83.29 | ...14-515,537-538 
  exitPlanMode.ts  |   82.43 |    76.36 |     100 |   82.43 | ...83-386,418-421 
  glob.ts          |   95.88 |     87.5 |    92.3 |   95.88 | ...16,172,303,306 
  grep.ts          |   83.09 |    86.66 |   80.95 |   83.09 | ...60-661,711-712 
  ...adTracking.ts |     100 |      100 |     100 |     100 |                   
  loop-wakeup.ts   |   99.24 |    92.85 |     100 |   99.24 | 44                
  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 |   80.51 |    78.46 |   84.44 |   80.51 | ...2981,2983-2984 
  mcp-client.ts    |   41.02 |    86.45 |   70.58 |   41.02 | ...1708,1712-1715 
  ...ry-timeout.ts |     100 |      100 |     100 |     100 |                   
  mcp-errors.ts    |     100 |      100 |     100 |     100 |                   
  ...pool-entry.ts |   77.21 |    83.96 |   79.41 |   77.21 | ...1259,1267-1268 
  ...ool-events.ts |       8 |        0 |       0 |       8 | 123-149           
  mcp-pool-key.ts  |   97.46 |    93.93 |     100 |   97.46 | 175-176           
  mcp-status.ts    |     100 |      100 |     100 |     100 |                   
  mcp-tool.ts      |   91.43 |     89.9 |   96.66 |   91.43 | ...72-673,723-724 
  ...sport-pool.ts |   83.27 |       80 |   84.61 |   83.27 | ...1399,1406-1410 
  ...ace-budget.ts |   87.27 |     82.6 |     100 |   87.27 | ...00-305,340-345 
  memory-config.ts |     100 |      100 |     100 |     100 |                   
  ...iable-tool.ts |     100 |    84.61 |     100 |     100 | 102,109           
  monitor.ts       |   91.65 |    84.05 |   88.46 |   91.65 | ...87,600,796-801 
  notebook-edit.ts |   85.11 |    76.42 |   81.25 |   85.11 | ...54-870,916-917 
  ...escendants.ts |   36.17 |    64.51 |   55.55 |   36.17 | ...46-310,385-390 
  ...nforcement.ts |   82.57 |    89.74 |     100 |   82.57 | 174-185,234-247   
  read-file.ts     |   94.75 |    90.32 |   81.81 |   94.75 | ...02,305,388-389 
  ripGrep.ts       |   93.85 |    85.21 |    87.5 |   93.85 | ...56-557,563-564 
  ...-transport.ts |    6.34 |        0 |       0 |    6.34 | 47-145            
  send-message.ts  |   79.48 |    86.95 |    62.5 |   79.48 | ...97-203,286-294 
  ...n-mcp-view.ts |   92.37 |    93.54 |   88.88 |   92.37 | 118-126           
  shell.ts         |   76.82 |    81.61 |   91.11 |   76.82 | ...4718,4781-4782 
  skill-utils.ts   |     100 |      100 |     100 |     100 |                   
  skill.ts         |    89.4 |     92.5 |   88.88 |    89.4 | ...43,447,476-498 
  ...eticOutput.ts |   95.12 |      100 |      80 |   95.12 | 87-88             
  task-create.ts   |   93.85 |     92.3 |   81.81 |   93.85 | 41-45,59-60,91    
  task-list.ts     |   73.38 |    77.77 |   83.33 |   73.38 | ...02,105,109-116 
  task-stop.ts     |   93.14 |    96.15 |   85.71 |   93.14 | 39-40,54-64       
  task-update.ts   |   80.67 |       78 |    92.3 |   80.67 | ...75-383,415-426 
  team-create.ts   |   97.22 |    85.71 |   83.33 |   97.22 | 48-49,129-130     
  team-delete.ts   |   86.74 |    83.33 |   83.33 |   86.74 | 37-38,42-48,72-73 
  todoWrite.ts     |   89.27 |    82.05 |   92.85 |   89.27 | ...50-555,577-578 
  tool-error.ts    |     100 |      100 |     100 |     100 |                   
  tool-names.ts    |     100 |      100 |     100 |     100 |                   
  tool-registry.ts |   76.19 |     76.1 |   81.39 |   76.19 | ...53-854,862-863 
  tool-search.ts   |   92.35 |    85.84 |    92.3 |   92.35 | ...08-213,320-329 
  tools.ts         |   92.36 |    90.74 |   90.47 |   92.36 | ...99-500,516-522 
  web-fetch.ts     |   89.56 |    83.33 |   92.85 |   89.56 | ...06-307,312-313 
  write-file.ts    |   82.65 |    80.45 |   84.61 |   82.65 | ...65-668,696-731 
 src/tools/agent   |   76.38 |    84.93 |   76.66 |   76.38 |                   
  agent.ts         |    76.6 |    85.14 |    77.1 |    76.6 | ...3097,3124-3187 
  fork-subagent.ts |   71.08 |       75 |   71.42 |   71.08 | ...25-126,161-172 
 ...s/computer-use |   90.03 |    81.27 |   75.36 |   90.03 |                   
  bootstrap.ts     |   59.42 |    80.95 |   41.66 |   59.42 | ...35-339,341-345 
  client.ts        |   73.22 |    89.65 |   64.28 |   73.22 | ...70-172,233-242 
  constants.ts     |     100 |    94.73 |     100 |     100 | 129,256           
  downloader.ts    |   65.29 |    52.77 |   58.33 |   65.29 | ...99-300,316-355 
  index.ts         |     100 |      100 |     100 |     100 |                   
  install-state.ts |   94.44 |    72.72 |     100 |   94.44 | 44-45             
  ...n-detector.ts |     100 |     87.5 |     100 |     100 | 50                
  schemas.ts       |     100 |      100 |     100 |     100 |                   
  tool.ts          |   96.24 |    85.29 |     100 |   96.24 | 75-76,184,251-257 
 ...tools/workflow |   85.51 |    79.01 |      90 |   85.51 |                   
  workflow.ts      |   85.51 |    79.01 |      90 |   85.51 | ...27-528,540-543 
 src/utils         |   90.02 |    88.58 |   94.65 |   90.02 |                   
  LruCache.ts      |       0 |        0 |       0 |       0 | 1-41              
  ...Controller.ts |     100 |      100 |     100 |     100 |                   
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...cFileWrite.ts |   94.76 |    93.26 |     100 |   94.76 | ...30-531,634-638 
  bareMode.ts      |   27.27 |      100 |       0 |   27.27 | 9-15,18-19        
  browser.ts       |   76.31 |    53.33 |     100 |   76.31 | ...37,43-44,65-66 
  btwUtils.ts      |   13.95 |      100 |       0 |   13.95 | 17-31,34-55       
  bundlePaths.ts   |     100 |      100 |     100 |     100 |                   
  ...ncyLimiter.ts |   94.64 |    95.23 |     100 |   94.64 | 64-66             
  ...igResolver.ts |     100 |      100 |     100 |     100 |                   
  ...engthError.ts |   91.11 |    89.47 |     100 |   91.11 | ...46-147,154-155 
  cronDisplay.ts   |     100 |    91.66 |     100 |     100 | 15,43,57          
  cronParser.ts    |   95.34 |     93.1 |     100 |   95.34 | 41-42,47-48,70-71 
  debugLogger.ts   |   96.42 |    94.11 |   88.23 |   96.42 | 185-189           
  editHelper.ts    |   93.63 |    83.52 |     100 |   93.63 | ...28-429,463-464 
  editor.ts        |    97.6 |     95.4 |     100 |    97.6 | ...25-326,328-329 
  ...arResolver.ts |   94.28 |    88.88 |     100 |   94.28 | 28-29,125-126     
  ...entContext.ts |   96.78 |    89.13 |      95 |   96.78 | ...51-252,257,403 
  errorParsing.ts  |    97.7 |    97.05 |     100 |    97.7 | 72-73             
  ...rReporting.ts |   88.46 |       90 |     100 |   88.46 | 69-74             
  errors.ts        |   70.54 |    80.39 |      50 |   70.54 | ...15-231,235-241 
  fetch.ts         |    70.8 |     77.5 |   71.42 |    70.8 | ...41-142,161,186 
  fileUtils.ts     |    91.5 |    86.25 |   95.23 |    91.5 | ...1191,1195-1201 
  forkedAgent.ts   |   80.68 |    78.12 |   83.33 |   80.68 | ...39-545,550-556 
  formatters.ts    |   81.81 |       75 |     100 |   81.81 | 15-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        |   31.57 |       50 |     100 |   31.57 | 26-38             
  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      |   72.91 |    90.32 |   83.33 |   72.91 | ...,77-78,102-153 
  iconvHelper.ts   |     100 |      100 |     100 |     100 |                   
  ...rePatterns.ts |     100 |      100 |     100 |     100 |                   
  ...ionManager.ts |     100 |     90.9 |     100 |     100 | 27                
  ...lPromptIds.ts |     100 |      100 |     100 |     100 |                   
  jsonl-utils.ts   |   90.12 |     92.3 |   91.66 |   90.12 | ...56-359,369-375 
  ...-detection.ts |     100 |      100 |     100 |     100 |                   
  ...iagnostics.ts |    96.4 |     94.2 |     100 |    96.4 | ...66,293-294,376 
  ...yDiscovery.ts |    92.4 |    89.01 |     100 |    92.4 | ...28,331,522-525 
  ...tProcessor.ts |   93.77 |    89.02 |     100 |   93.77 | ...13-319,406-407 
  ...Inspectors.ts |   61.53 |      100 |      50 |   61.53 | 18-23             
  modelId.ts       |   98.96 |    98.18 |     100 |   98.96 | 153               
  ...kerChecker.ts |    90.9 |    91.66 |     100 |    90.9 | 73-79             
  notebook.ts      |   94.57 |    89.83 |   95.83 |   94.57 | ...21,333,385-387 
  openaiLogger.ts  |   90.85 |    87.87 |     100 |   90.85 | ...97-199,222-227 
  partUtils.ts     |     100 |    98.61 |     100 |     100 | 206               
  pathReader.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |    93.3 |    92.22 |     100 |    93.3 | ...92-393,395-397 
  pdf.ts           |   93.68 |    87.05 |     100 |   93.68 | ...96-297,321-325 
  projectPath.ts   |     100 |      100 |     100 |     100 |                   
  projectRoot.ts   |   71.73 |    78.57 |     100 |   71.73 | 54-66             
  ...ectSummary.ts |   89.62 |    72.41 |     100 |   89.62 | ...40-145,196-199 
  ...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     |   93.75 |    88.34 |     100 |   93.75 | ...13,218-219,262 
  readManyFiles.ts |   87.59 |       84 |     100 |   87.59 | ...09-211,227-238 
  retry.ts         |   95.93 |    91.83 |     100 |   95.93 | ...33,524-525,543 
  retryContext.ts  |     100 |      100 |     100 |     100 |                   
  ...sification.ts |   97.52 |    96.84 |     100 |   97.52 | ...05,255-256,282 
  retryPolicy.ts   |   97.72 |    90.56 |     100 |   97.72 | 130-131           
  ripgrepUtils.ts  |   50.94 |    85.71 |      70 |   50.94 | ...54-255,268-346 
  ...sDiscovery.ts |   97.42 |    92.85 |     100 |   97.42 | ...04,182-183,202 
  ...iagnostics.ts |   83.08 |     67.5 |   92.59 |   83.08 | ...23,543-544,550 
  ...tchOptions.ts |   82.18 |    85.18 |   95.23 |   82.18 | ...24,549,578-587 
  ...odelPrefix.ts |     100 |      100 |     100 |     100 |                   
  runtimeStatus.ts |    97.5 |    89.47 |     100 |    97.5 | 162-163           
  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 |   91.97 |    83.42 |     100 |   91.97 | ...44,866-867,880 
  ...r-launcher.ts |   96.35 |    93.97 |   85.71 |   96.35 | ...35-336,347-348 
  sedEditParser.ts |   91.72 |    92.12 |     100 |   91.72 | ...36-539,615-616 
  ...nIdContext.ts |     100 |      100 |     100 |     100 |                   
  ...orageUtils.ts |   95.98 |     83.8 |     100 |   95.98 | ...70,386,466,485 
  shell-utils.ts   |   86.24 |    89.61 |     100 |   86.24 | ...2003,2010-2014 
  ...lAstParser.ts |   95.57 |    85.88 |     100 |   95.57 | ...1066-1068,1078 
  ...ContextEnv.ts |     100 |      100 |     100 |     100 |                   
  ...nlyChecker.ts |   95.08 |    91.66 |     100 |   95.08 | ...15-316,324-325 
  sideQuery.ts     |   86.61 |     87.5 |     100 |   86.61 | ...66-172,174-180 
  ...pEventSink.ts |     100 |       80 |     100 |     100 | 61                
  ...tGenerator.ts |     100 |      100 |     100 |     100 |                   
  ...ameContext.ts |     100 |      100 |     100 |     100 |                   
  symlink.ts       |   81.48 |    77.77 |     100 |   81.48 | 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     |      65 |      100 |      75 |      65 | 56-75             
  thoughtUtils.ts  |     100 |    92.85 |     100 |     100 | 71                
  ...-converter.ts |   95.23 |    85.71 |     100 |   95.23 | 36-37             
  tool-utils.ts    |    93.6 |     91.3 |     100 |    93.6 | ...58-159,162-163 
  ...ultCleanup.ts |   15.74 |    33.33 |      25 |   15.74 | 33-134            
  ...Compaction.ts |   95.68 |    95.32 |     100 |   95.68 | ...29-334,533-534 
  truncation.ts    |   75.55 |    86.02 |   71.42 |   75.55 | ...44-449,453-477 
  windowsPath.ts   |   89.47 |    79.31 |     100 |   89.47 | ...57-58,62,90-91 
  ...aceContext.ts |   95.81 |    89.39 |     100 |   95.81 | ...74-275,299-301 
  xml.ts           |    97.8 |     87.5 |     100 |    97.8 | 98-99             
  yaml-parser.ts   |   83.87 |    77.27 |     100 |   83.87 | ...31-234,239-240 
 ...ils/filesearch |   83.26 |    80.07 |   94.44 |   83.26 |                   
  crawlCache.ts    |     100 |      100 |     100 |     100 |                   
  crawler.ts       |   82.47 |    76.22 |      95 |   82.47 | ...1525,1559-1560 
  fileSearch.ts    |   93.78 |    87.67 |     100 |   93.78 | ...70-271,273-274 
  fzfWorker.ts     |       0 |        0 |       0 |       0 | 1-109             
  ...rkerHandle.ts |   84.05 |    75.43 |   89.47 |   84.05 | ...30-334,340-341 
  ignore.ts        |     100 |      100 |     100 |     100 |                   
  result-cache.ts  |     100 |    93.75 |     100 |     100 | 49                
 ...uest-tokenizer |   68.81 |    73.82 |   83.87 |   68.81 |                   
  ...eTokenizer.ts |   65.72 |    74.02 |    92.3 |   65.72 | ...65-466,479-533 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tTokenizer.ts |   68.39 |    69.49 |    90.9 |   68.39 | ...24-325,327-328 
  ...ageFormats.ts |   76.92 |      100 |   33.33 |   76.92 | 46-49,56-57       
  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.

克竟 added 8 commits June 8, 2026 19:18
Match CC's "Add Marketplace" screen: a bold "Add Marketplace" header,
an "Enter marketplace source:" prompt, and an "Examples:" bullet list
(owner/repo · git@…:owner/repo.git (SSH) · https://…/marketplace.json ·
./path/to/marketplace) above a bare cursor input (placeholder removed).

Update the Marketplaces add-view tests accordingly.
…t with CC

Discover reliability and UX fixes:

- Fix the "Discovering plugins…" hang: marketplace network fetches had no
  timeout, so a slow/unreachable source could block discovery forever. Add a
  10s per-request timeout (resolve null) plus socket drain on non-200.
- Cache the fetched listing in ExtensionManager for the session so revisiting
  the tab no longer refetches over the network; `installed` flags are
  recomputed cheaply, and the cache is invalidated on add/remove marketplace.

CC-aligned Discover list:

- Windowed/scrolling viewport (no longer renders the entire 200+ list at
  once) with "↑ more above" / "↓ more below" hints, sized to the terminal.
- Type-to-search filter with a search box and a "Discover plugins (pos/total)"
  count header.
- Item layout: cursor "›", ○/●/✓ checkbox, bold title · marketplace ·
  "<N> installs", with a truncated description line.
- Space toggles selection, Enter views detail (or installs the selected set);
  the conflicting "i" shortcut was removed in favor of search.

Core: DiscoveredPlugin gains a best-effort `installs` count.

Adds tests for windowing, search filtering, and install-count extraction.
Rework the Marketplaces tab detail to match Claude Code:

- Show marketplace name, source, "N available plugins", and the plugins
  from this marketplace that are installed ("Installed plugins (K):" with
  descriptions) — instead of dumping a truncated list of all plugins.
- Replace the ad-hoc "d to remove" hint with an action selector:
  Browse plugins (N) · Update marketplace [(last updated DATE)] ·
  Remove marketplace.
- "Browse plugins" switches to the Discover tab filtered to this
  marketplace (only its plugins); the filter clears on manual tab switch
  and is shown in the Discover header.
- "Update marketplace" re-fetches the marketplace config, stamps a fresh
  "last updated", and invalidates the discovery cache.

Core: MarketplaceSource gains lastUpdatedAt; addMarketplace stamps it and
ExtensionManager.markMarketplaceUpdated() refreshes it + clears the
discovery cache.

Adds tests for the marketplace detail layout and the Browse-to-Discover
filtering flow.
…on adds

- Installed list: plugin rows now show their type + version ("Extension
  v0.7.0"), parallel to MCP rows ("MCP"), instead of a bare version.
- Add Marketplace: when the source is not a Claude marketplace but is a
  valid single extension source (Gemini/Claude/git/npm), the error now
  guides the user to install it directly ("... looks like a single
  extension, not a marketplace. Install it with: /extensions install X")
  instead of the generic "expected marketplace.json" message.
The Marketplaces add flow advertises git@github.com:owner/repo.git (SSH)
as a supported format, but loadMarketplaceConfigFromSource relied on
parseGitHubRepoForReleases, which rejects the git@ scp-like form. Extract
owner/repo directly from the git@github.com:owner/repo(.git) form before
falling back to the URL parser, so SSH marketplace sources actually
resolve. Adds a regression test.
…ketplaces tab

- Terminology: use 'Extension' instead of 'Plugin' across the dialog
  (Discover extensions, Extension details, Back to extension list, etc.).
- Tabs reordered to Installed, Discover, Marketplaces; the dialog now
  opens on Installed by default.
- Marketplaces tab is now a sources hub:
  - new 'Install new extension' action (installs a single Gemini/Qwen/
    Claude/git/npm extension directly via parseInstallSource).
  - 'Add new marketplace' annotated as a Claude plugin marketplace.
  - items grouped into 'Extensions' and 'Marketplaces' sections; an
    extension row opens a compact detail with Uninstall.

Updates the dialog tests for the new wording, tab order and layout.
克竟 added 7 commits June 9, 2026 11:39
… Marketplaces footer

- Add a shared ExtensionActionsView (info + components + action menu +
  scope-select + uninstall-confirm) used by both the Installed and
  Marketplaces tabs, so the Marketplaces extension detail now offers the
  full set (Enable/Disable, Favorite, Mark for Update, Update Now,
  Uninstall) instead of just Uninstall/Back.
- Add a new 'Change scope' action (Global/Project/Local) that re-scopes
  enablement (User vs workspace), available in both tabs.
- Context-aware Marketplaces footer: shows 'Enter details' for an
  extension row, 'Enter open · d remove marketplace' for a marketplace
  row, and a neutral hint for the action rows — no longer says
  'd remove marketplace' when an extension is selected.

Adds a test for the full extension actions in the Marketplaces detail.
…+ fix enable/disable in Sources detail

- Rename the user-visible tab label 'Marketplaces' -> 'Sources' (TabBar +
  TABS). The in-tab 'Marketplaces' section header (grouping marketplace
  sources) is kept. Also update the Discover empty-state hint to point at
  the 'Sources' tab.
- Hide the Add/Remove Favorites action in the Sources extension detail via
  a showFavorite prop (default true; Installed keeps it).
- Fix a stale enable/disable label in the Sources extension detail:
  ExtensionActionsView re-read enablement through the manager cache keyed by
  a tick, but refreshCache() briefly empties that cache, so the read raced
  and fell back to the stale extension prop (isActive: true). It now holds
  authoritative local state (enabled/isFavorite/scope) updated optimistically
  after each action — no cache read-back. The Installed tab was immune only
  because it fed a fresh extension object each load.

Adds regression tests for the enable/disable toggle staying in sync and for
change-scope re-scoping + re-enabling a disabled extension.
…elector

- Add an 'Add new' section heading above the '+ Install new extension' and
  '+ Add new marketplace' rows on the Sources tab, so those two actions are
  grouped like the Extensions and Marketplaces sections.
- In the Change scope selector, default the cursor to the extension's current
  scope and show a 'Current: <scope>' line. Previously it always defaulted to
  Global, so after changing scope it was unclear whether the change took
  effect. Applies to both the Sources and Installed extension detail (shared
  ExtensionActionsView).

Updates tests to assert the 'Add new' section title renders and that
re-entering the scope selector reflects the now-current scope.
…TW i18n

- Move the 'Note: Uninstall permanently removes this extension.' warning out
  of the detail-view action list and into the uninstall confirmation step
  (replacing the near-synonymous 'This action cannot be undone.').
- Fix the Chinese/English mix in the extensions manager: the new multi-tab UI
  added ~104 English strings that had no locale entries, so they fell back to
  the English key at runtime. Add Simplified (zh) and Traditional (zh-TW)
  translations for all of them, plus the matching en.js base keys (en.js is
  the canonical superset; zh/zh-TW require strict key parity per check-i18n).

Placeholders, keyboard tokens (Tab/Enter/Esc/Space/↑↓/·) and the ⚠ glyph are
preserved across all locales.
Rename the Installed-tab scope group headers from User/Project/Local to
'X level' (用户级/项目级/本地级) so the grouping reads as scope levels.
Adds the new keys to en/zh/zh-TW locales.
The Installed-tab MCP item detail was a read-only view (name/type/scope/
transport/status) with a meaningless 'Enter to select' and no actions. Replace
it with McpServerActionsView, which reuses the /mcp dialog's ServerDetailStep,
ToolListStep, ToolDetailStep and AuthenticateStep so the behaviour matches
/mcp exactly: live connection status, View tools, Enable/Disable, Reconnect
(when disconnected), Re-authenticate and Clear authentication.

Handlers mirror MCPManagementDialog (mcp.excluded settings + toolRegistry
discover/disable/disconnect + MCPOAuthTokenStorage). Delete the now-unused
McpDetailView and its obsolete locale keys; add the two new status strings to
en/zh/zh-TW.
Review follow-up: buildServer hardcoded promptCount to 0, diverging from
/mcp's fetchServerData. Query the prompt registry like the original so the
reused MCPServerDisplayInfo is computed consistently.
@BZ-D BZ-D changed the title feat(extensions): multi-tab /extensions dialog (Discover/Installed/Marketplaces) feat(extensions): interactive multi-tab /extensions manager (Installed / Discover / Sources) Jun 9, 2026
克竟 added 3 commits June 9, 2026 17:35
…lace to source

The Sources tab treats both single-extension sources and Claude plugin
marketplaces as 'sources', so the source-management layer is renamed for
consistency:
  MarketplaceSource        -> ExtensionSource
  marketplaceRegistry(.ts) -> sourceRegistry(.ts)
  MarketplaceRegistryStore -> SourceRegistryStore
  add/get/remove/markMarketplaceUpdated -> add/get/remove/markSourceUpdated
  loadMarketplace/updateMarketplace     -> loadSource/updateSource
  MarketplacesTab -> SourcesTab; EXTENSIONS_TABS.MARKETPLACES -> SOURCES
  + the source-detail UI handlers.

Terms that refer to the Claude marketplace manifest *format* are kept, since a
marketplace is one source type: ClaudeMarketplaceConfig,
loadMarketplaceConfigFromSource, the .claude-plugin/marketplace.json path,
DiscoveredPlugin.marketplaceName, and the in-tab 'Marketplaces' group label.
…vive the rename

The source/* rename accidentally renamed the persisted registry file from
marketplaces.json to sources.json, so previously added sources (e.g. a Claude
marketplace) appeared to vanish — the data was intact in marketplaces.json but
the code read sources.json. Restore the marketplaces.json filename for
backward compatibility.
Previously runInstall always returned to the list after attempting an install.
Now it only returns to the list on success; on failure it stays on the
extension detail page so the error message remains visible and the user can
retry without re-navigating.
res.on('error', () => done(null));
});
req.on('error', () => done(null));
req.setTimeout(MARKETPLACE_FETCH_TIMEOUT_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] req.setTimeout is a socket-idle timeout, not an absolute wall-clock deadline — the timer resets every time a chunk is received. A server trickling 1 byte every 9 seconds will never trigger the 10-second idle timer, causing fetchUrl (and thus the entire Discover tab via Promise.all across all sources) to hang indefinitely with no user-visible diagnostic beyond a permanent "Loading..." spinner.

Suggested change
req.setTimeout(MARKETPLACE_FETCH_TIMEOUT_MS, () => {
req.setTimeout(MARKETPLACE_FETCH_TIMEOUT_MS, () => {
req.destroy();
done(null);
});
// Hard wall-clock deadline — req.setTimeout only resets on socket activity.
const hardTimer = setTimeout(() => {
req.destroy();
done(null);
}, MARKETPLACE_FETCH_TIMEOUT_MS);
req.on('close', () => clearTimeout(hardTimer));

— qwen3.7-max 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 05eb44f — added an absolute wall-clock deadline (a setTimeout cleared in done()) alongside the existing socket-idle req.setTimeout.

{extensions.length === 0 && sources.length === 0 ? (
<Box marginTop={1}>
<Text color={theme.text.secondary}>
{t('No extensions or sources added yet.')}

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] i18n key mismatch: this calls t('No extensions or sources added yet.') but all three locale dictionaries (en.js, zh.js, zh-TW.js) define the key as 'No extensions or marketplaces added yet.'. The translation lookup misses, so Chinese (zh/zh-TW) users see the raw English key string instead of the localized text.

Suggested change
{t('No extensions or sources added yet.')}
{t('No extensions or marketplaces added yet.')}

— qwen3.7-max 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.

Couldn't reproduce on current HEAD — the empty state renders t('No marketplaces added yet.') (defined in en/zh/zh-TW), and 'No extensions or sources added yet.' doesn't appear anywhere in the tree. Looks fixed in a push after the reviewed SHA.

…places

Some Claude marketplace plugins live in a subdirectory of a git repo and use a
'git-subdir' source ({url, path, ref, sha}), which resolvePluginSource didn't
handle — installing failed with 'Unsupported plugin source type'. Add the
git-subdir branch: clone the repo (pinned to ref/sha when provided) and return
the subdirectory as the plugin source.

Verified against github.com/42Crunch-AI/claude-plugins @ v1.5.5: the cloned
plugins/api-security-testing subdir is a valid plugin (.claude-plugin/plugin.json).

@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.

[Suggestion] The new git-subdir source type in resolvePluginSource has zero test coverage. No existing tests exercise any object-type source (github, url, or git-subdir) — the function is only tested via local-directory paths. At minimum, add integration tests (via convertClaudePluginPackage) covering: successful clone with ref/sha/neither, subdirectory-not-found error, and clone-failure propagation.

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

originSource: 'Claude',
};
await cloneFromGit(installMetadata, pluginDir);
const subDir = path.join(pluginDir, source.path);

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] Path traversal via unvalidated source.path. path.join(pluginDir, source.path) does not confine the resolved path to pluginDir — a malicious marketplace manifest can set path: "../../.ssh" to escape the sandbox. fs.existsSync only checks whether the escaped path exists, not whether it lies within pluginDir.

Suggested change
const subDir = path.join(pluginDir, source.path);
const subDir = path.resolve(pluginDir, source.path);
const resolvedBase = path.resolve(pluginDir);
if (!subDir.startsWith(resolvedBase + path.sep) && subDir !== resolvedBase) {
throw new Error(
`Plugin path "${source.path}" escapes the plugin directory`,
);
}
if (!fs.existsSync(subDir)) {

Also validate that source.path is non-empty — an empty string causes path.join to return pluginDir itself, silently installing the repo root instead of a subdirectory.

— 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 05eb44fsource.path is confined to the clone via path.resolve + startsWith(repoRoot + sep), and empty/./absolute values are rejected before use.

if (source.source === 'git-subdir') {
// The plugin lives in a subdirectory of a git repository. Clone the repo
// (pinned to the provided ref/sha when present) and return the subdir.
const installMetadata: ExtensionInstallMetadata = {

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] SHA-based clone incompatible with shallow clones on non-GitHub remotes. cloneFromGit does --depth 1 shallow clone then git.fetch(remote, sha). Fetching by SHA from a shallow clone requires server-side uploadpack.allowReachableSHA1InWant support, which self-hosted GitLab, Gitea, and Bitbucket Server may not enable. When a marketplace manifest specifies sha (the precise, immutable pin), installation silently fails with a generic Failed to clone Git repository error.

Consider either using git fetch --depth=... origin <sha> instead of the shallow-clone-then-fetch pattern, or documenting this limitation clearly so marketplace authors know SHA pins only work on GitHub.

— 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.

Deferring — narrow edge (only when a manifest pins sha AND the remote lacks allowReachableSHA1InWant). I'll document the limitation / add an unshallow fallback in a follow-up.

const installMetadata: ExtensionInstallMetadata = {
source: source.url,
type: 'git',
ref: source.ref || source.sha,

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] ref silently takes precedence over sha when both are present. ref: source.ref || source.sha means sha is ignored if ref is truthy, but the TypeScript type allows both fields to coexist. A future maintainer reading the type may assume sha acts as a hard constraint, while the runtime silently discards it.

Consider either making ref and sha mutually exclusive in the type ({ ref?: string } | { sha?: string }), or adding a runtime check that warns when both are provided.

— 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 05eb44f — now ref: source.sha || source.ref, so the immutable SHA pin takes precedence.

const subDir = path.join(pluginDir, source.path);
if (!fs.existsSync(subDir)) {
throw new Error(
`Plugin subdirectory "${source.path}" not found in ${source.url}`,

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] Error message for missing subdirectory does not include the ref/sha used for the clone. When source.path doesn't exist in the cloned repo, the error only reports the URL — if the user specified ref: "main" but the subdirectory only exists on a feature branch, they have no way to diagnose this from the error.

Include the ref in the error: `Plugin subdirectory "${source.path}" not found in ${source.url} (ref: ${source.ref || source.sha || 'HEAD'})`

— 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 05eb44f — the not-found error now includes (ref: ${source.ref ?? source.sha ?? 'HEAD'}).

Simplify the install/visibility scope model from three options (user /
project / local) down to two (user / project). The 'local' option duplicated
the workspace-level enablement of 'project' without providing a meaningfully
different storage location, so it was UI clutter rather than a real feature.

- core: ExtensionScope = 'user' | 'project'; read() filters unknown values
  via a type guard, so any stale 'local' (or otherwise invalid) entry in
  extension-preferences.json is dropped and falls back to 'user' downstream.
- UI: remove the 'local' option from the Discover install menu, the change-
  scope picker, and the Installed tab's group ordering.
- copy: rename 'Project (All Collaborators)' to 'Project (Workspace)' and
  'Install for all collaborators on this repository' to 'Install for the
  current workspace', matching the new two-tier model.
- i18n: clean up the now-unused 'Local *' keys in en / zh / zh-TW and
  retranslate the renamed keys.
- tests: update the scope-change spec and replace the legacy-scope
  migration test with one that exercises the unknown-value filter.
</Text>
</Box>
<Text bold color={titleColor}>
{plugin.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.

[Critical] ANSI escape sequence injection via marketplace plugin metadata. Plugin name, description, author, and marketplaceName from marketplace JSON are rendered directly inside Ink <Text> components without any sanitization. A malicious marketplace can embed ANSI escape codes (e.g., \x1b[2K\x1b[1A) in these fields to clear terminal lines, move the cursor, erase security warnings, or spoof UI elements.

This affects DiscoverTab, SourcesTab, and PluginDetailView — all render marketplace-sourced strings verbatim.

Suggested change
{plugin.name}
// Strip ANSI from all marketplace-sourced strings before rendering.
// Use strip-ansi or a simple regex: /\x1b\[[0-9;]*[a-zA-Z]/g
const safeName = plugin.name.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');

— qwen3.7-plus 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 05eb44f — marketplace strings are stripped of ANSI/VT/control chars at the pluginsFromConfig boundary (covers the Discover list + detail), and the Sources detail wraps untrusted names in stripUnsafeCharacters. Install resolution still uses the raw fields, so this is display-only.

return;
}
const chunks: Buffer[] = [];
res.on('data', (chunk) => chunks.push(chunk));

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] Unbounded HTTP response body in fetchUrl enables OOM via malicious marketplace. The chunks array accumulates the entire response without any size cap. A malicious marketplace server can stream gigabytes of data within the 10-second timeout window, exhausting process memory.

Before this PR, fetchUrl was only called against GitHub API and raw.githubusercontent.com. Now it fetches arbitrary HTTPS URLs supplied as marketplace sources, and these are re-fetched every time the user opens the Discover tab.

Suggested change
res.on('data', (chunk) => chunks.push(chunk));
const MAX_BODY = 5 * 1024 * 1024; // 5 MB
let totalBytes = 0;
res.on('data', (chunk) => {
totalBytes += chunk.length;
if (totalBytes > MAX_BODY) {
req.destroy();
done(null);
return;
}
chunks.push(chunk);
});

— qwen3.7-plus 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 05eb44f — the response body is capped at 10 MB (the request is destroyed and resolves null once exceeded).

try {
manager.setExtensionScope(name, newScope);
// Apply enablement: Global -> User; Project/Local -> workspace only.
if (newScope === 'user') {

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] Scope change from project to user does not disable workspace enablement. When newScope === 'user', the code only calls enableExtension(name, SettingScope.User) but does NOT call disableExtension(name, SettingScope.Workspace) first. The reverse direction (user → project) correctly disables User then enables Workspace.

After a project-to-user scope change, the extension has enablement overrides at both scopes. If the user later disables the extension via the Installed tab (which targets User scope), the stale Workspace enablement persists and the extension remains active.

Suggested change
if (newScope === 'user') {
if (newScope === 'user') {
await manager.disableExtension(name, SettingScope.Workspace);
await manager.enableExtension(name, SettingScope.User);
} else {

— qwen3.7-plus 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.

I believe this is intentional rather than a bug: switching to user scope is a global enable, and setExtensionScope already records the scope intent. The user→project branch disables User precisely because workspace-only enablement requires it. Happy to revisit with a concrete stale-override repro.

if (src && src.source === 'url') {
return src.url;
}
return plugin.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] resolveInstallSource does not handle the git-subdir source variant. This PR adds a git-subdir variant to ClaudePluginSource (in claude-converter.ts), but resolveInstallSource only handles string, { source: 'github' }, and { source: 'url' }. A plugin with { source: 'git-subdir', url, path, ref? } falls through all conditions and returns just plugin.name — a bare name that is not a valid install source.

The Discover tab would show the plugin but installing it would fail with a parse error.

Suggested change
return plugin.name;
if (src && src.source === 'git-subdir') {
return `${src.url}:${plugin.name}`; // or filter out during discovery
}
return plugin.name;

— qwen3.7-plus 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.

Valid gap, but deferring: the suggested return src.url would clone+install the repo root, silently dropping path/ref — worse than today's clear Install source not found. Proper support needs the subdir/ref carried through the install metadata. Only affects git-subdir plugins served by an http-type marketplace; tracking as a follow-up.

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.

Thanks — I dug into this again rather than just re-deferring, and I think the return src.url one-liner would introduce a silent wrong-install, so I'd like to push back and propose a proper follow-up instead.

Why return src.url is unsafe here: a git-subdir source is { url, path, ref?, sha? } — the plugin lives in the subdirectory path of the repo, optionally pinned to ref/sha. But ExtensionInstallMetadata (config.ts:540) only has source / type / ref — there is no subdirectory field, and parseInstallSource has no subdir syntax. So return src.url drops both path and the pin, and the installer clones the repo root: it then either installs the whole repo as a standalone extension or (if the repo happens to ship its own marketplace.json) prompts the user to pick an unrelated plugin. In every case it installs something other than the subdir plugin the entry describes — strictly worse than today's explicit "not found" failure.

Scope of the actual gap: string- and github-form sources from an HTTP marketplace already install git-subdir plugins correctly, because their installSource is a repo:plugin form that routes back through convertClaudePluginPackageresolvePluginSource, which fully handles git-subdir (now covered by the new tests in 158002c77). The unsupported case is narrowly a direct { source: 'git-subdir' } entry in an http-type (direct-JSON) marketplace.

Proper fix: thread the subdirectory + ref through the install pipeline — add a path/subdir field to ExtensionInstallMetadata and teach the git clone to check out the subdir at the pinned ref. That's a self-contained capability with its own tests, which I'd prefer to land as a follow-up rather than fold into this already-large PR. If you'd rather have it in-scope here, say the word and I'll add it (with tests) in the next push.

item.name,
]);
}
await toolRegistry?.disableMcpServer(item.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] toggleMcp enable path updates runtime excluded list but disable path does not. When enabling an MCP server, the code explicitly removes the server from the runtime excluded list via config.setExcludedMcpServers(...). When disabling, it only calls toolRegistry?.disableMcpServer(name) without updating the runtime excluded list.

Other code that checks config.getExcludedMcpServers() to determine if a server is disabled may get an incorrect answer after toggling disable from this UI.

Suggested change
await toolRegistry?.disableMcpServer(item.name);
// In the disable branch (item.isActive === true), add:
const runtimeExcluded = config.getExcludedMcpServers() ?? [];
if (!runtimeExcluded.includes(item.name)) {
config.setExcludedMcpServers([...runtimeExcluded, item.name]);
}
await toolRegistry?.disableMcpServer(item.name);

— qwen3.7-plus 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.

Couldn't reproduce — the disable branch persists mcp.excluded to settings, and toolRegistry.disableMcpServer() itself appends the server to config.getExcludedMcpServers() (tool-registry.ts), so the runtime list is updated on disable.

try {
const metadata = await parseInstallSource(plugin.installSource);
const ext = await extensionManager.installExtension(metadata);
extensionManager.setExtensionScope(ext.name, scope);

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] setExtensionScope failure inside install loop marks a successfully-installed extension as failed. If setExtensionScope throws after installExtension succeeds, the catch block counts the entire operation as a failure even though the extension is fully installed and functional. The user sees an error like "Installed 0, failed 1" and may attempt to re-install.

Suggested change
extensionManager.setExtensionScope(ext.name, scope);
const ext = await extensionManager.installExtension(metadata);
try {
extensionManager.setExtensionScope(ext.name, scope);
} catch (scopeError) {
debugLogger.error('Failed to record scope preference:', scopeError);
// Extension is installed; scope preference is non-critical.
}

— qwen3.7-plus 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 05eb44f — install and the post-install scope/enablement step are now separate try/catch blocks; a scope-preference failure is logged but no longer flips a successful install to "failed".

}
const src = plugin.source;
if (typeof src === 'string') {
return src.includes(':') ? src : `${src}:${plugin.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] Remote http marketplace can reference arbitrary local paths via resolveInstallSource. For http-type marketplaces, when plugin.source is a string containing :, it's returned as-is. A malicious marketplace JSON could declare "source": "/opt/internal-tool"parseInstallSource then calls stat(repo) which succeeds for existing paths, treating it as a local extension.

A remote marketplace should not be able to reference local filesystem directories.

Suggested change
return src.includes(':') ? src : `${src}:${plugin.name}`;
if (typeof src === 'string') {
if (path.isAbsolute(src)) {
debugLogger.warn(`Rejecting local path source from remote marketplace: ${src}`);
return plugin.name;
}
return src.includes(':') ? src : `${src}:${plugin.name}`;
}

— qwen3.7-plus 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 05eb44f — string sources that are absolute or start with ./~ are rejected for remote marketplaces (returns the bare name → clear error rather than a local install).

return;
}
try {
await open(plugin.homepage);

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] openHomepage accepts file:// URLs from marketplace manifest. The function passes plugin.homepage directly to open() without validating the URL scheme. The open package supports file:// URLs, which opens the referenced local file in the OS-default application.

A malicious marketplace could set "homepage": "file:///etc/passwd" or "homepage": "file:///Users/victim/.ssh/id_rsa".

Suggested change
await open(plugin.homepage);
const openHomepage = useCallback(
async (plugin: DiscoveredPlugin) => {
if (!plugin.homepage) return;
try {
const url = new URL(plugin.homepage);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
onStatus({ type: 'error', text: t('Failed to open {{url}}', { url: plugin.homepage }) });
return;
}
await open(plugin.homepage);
} catch {

— qwen3.7-plus 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 05eb44fopenHomepage now parses the URL and only follows http:/https:.

settled = true;
resolve(value);
};
const req = https.get(url, { headers }, (res) => {

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] fetchUrl uses node:https exclusively — http:// marketplace URLs silently fail. loadMarketplaceConfigFromSource accepts both http:// and https:// prefixes (Priority 2 branch), but fetchUrl only imports node:https. When a user adds an http:// marketplace (e.g., a local network server), https.get('http://...') produces a TLS error that is silently caught and resolved as null. The user sees only "Could not load this marketplace." with no diagnostic.

Either import node:http and dispatch based on URL protocol, or reject http:// URLs early with a clear error message.

— qwen3.7-plus 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.

Deferring — http:// marketplaces are an insecure transport; rather than silently failing I'll either reject them with a clear message or add explicit node:http dispatch in a follow-up.

克竟 added 2 commits June 10, 2026 17:48
Press Ctrl+R in the Extensions Manager Discover tab to bypass the discover cache and re-fetch all marketplace sources. The refresh hint is merged into the dialog footer, and a success status is shown after the refresh completes.
The Discover tab list reused the global SELECTION_UP/DOWN matchers,
which include bare j/k as Vim-style navigation. Combined with
type-to-search input, that made it impossible to type j or k into the
search query.

Switch the Discover list navigation to explicit arrow keys plus
Ctrl+P/Ctrl+N, so bare j/k fall through to the printable-character
branch and append to the query. Other extension tabs (Installed,
Sources) remain pure lists and keep the Vim navigation.
@BZ-D

BZ-D commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough macOS real-build verification @wenshao 🙏 — especially the byte-level confirmation that the OSC title-injection is neutralized pre-consent.

Followed up on your two non-blocking notes:

1. sanitizeDisplay() had no direct unit test — fixed in a2d63d780. Added a discoverPlugins test that feeds a hostile plugin through the real boundary and asserts every rendered field comes out clean. It mirrors your PoC payload exactly (CSI color/blink, cursor-up [1A, line-clear [2K, OSC ]0;PWNED+BEL, reverse-video [7m):

  • name evil\x1b[31m-plugin\x1b[2Kevil-plugin
  • version 9.9.9\x1b[5m9.9.9
  • description Totally safe\x1b[1A\x1b]0;PWNED\x07 — trust me\x1b[7mTotally safe — trust me
  • author mallory\x1b[0mmallory
  • homepage, category, lastUpdated, the component name, and the marketplace name are all asserted scrubbed, plus a belt-and-braces check that no \x1b/BEL byte survives in any field.

This locks in the sanitization at the discoverPlugins sink (not just the shared stripAnsiAndControl util, which already has its own tests in textUtils.test.ts). sourceRegistry.test.ts is now 16/16 green.

2. The $HOME-anchored disable glob — agreed, that's the pre-existing extensionManager user-scope override semantics (!<homedir>/*), only a test-environment artifact when the workspace is a sibling of a fake $HOME or across a /tmp/private/tmp realpath split. Nothing to change in this PR; noting it so the next verifier doesn't misread it.

Also re the red Test (macos-latest): as you confirmed, it's the unrelated cronScheduler durable-lock timing flake (this branch touches zero cron files, green on ubuntu, 89/89 locally ×5).

If there's nothing else outstanding, an approval would be much appreciated 🙏 — happy to iterate on anything that remains.

中文

多谢 @wenshao 在 macOS 上的真实构建验证 🙏,尤其是字节级确认了 OSC 改标题注入在安装前已被消解。

两条非阻塞提示都已跟进:

1. sanitizeDisplay() 缺直接单测 —— 已在 a2d63d780 补上。discoverPlugins 套件加了一个测试,把恶意插件喂过真实边界,断言每个展示字段输出干净;payload 完全复用你验证报告里的攻击载荷(CSI 颜色/闪烁、光标上移 [1A、清行 [2K、OSC ]0;PWNED+BEL、反显 [7m)。name/version/description/author/homepage/category/lastUpdated、组件名、以及 marketplace 名都断言已清洗,另加一条"任何字段都不残留 \x1b/BEL"的兜底检查。这把清洗锁在了 discoverPlugins 这个 sink 上(共享的 stripAnsiAndControl 工具本身在 textUtils.test.ts 已有测试)。sourceRegistry.test.ts 现在 16/16 全绿。

2. $HOME 锚定的禁用 glob —— 同意,这是 extensionManager 既有的 user-scope override 语义(!<homedir>/*),只在工作区是 fake $HOME 兄弟目录、或踩到 /tmp/private/tmp realpath 差异时才像"没生效";本 PR 无需改动,仅作提示。

另外红色的 Test (macos-latest) 正如你确认的,是无关的 cronScheduler 锁竞态 flaky(本分支没碰任何 cron 文件,ubuntu 全绿,本地 5 次 89/89)。

如无其他问题,烦请帮忙 approve 🙏。

@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.

[Suggestion] Stale JSDoc on sanitizeForErrorMessage in workflow-orchestrator.ts: the JSDoc still says "Control characters are replaced with a single space" but the new stripAnsiAndControl removes them entirely (empty string). The inner comment acknowledges this ("Removing rather than spacing"), but the outer JSDoc was not updated. The call-site comment at ~line 487 has the same stale description.

— qwen3.7-max via Qwen Code /review

// A transient read error (permission/too-many-files/…) — the file may be
// perfectly valid, so do NOT quarantine it here; only parse failures
// above do that. Return the default for this read.
debugLogger.error('Error reading extension preferences:', 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.

[Suggestion] Silent empty state on transient read errors. Both this catch and the analogous one in sourceRegistry.ts only log via debugLogger.error(), which is gated behind QWEN_DEBUG_LOG_FILE. When a transient error (EACCES/EMFILE/EISDIR) hits, the user's favorites/scopes/disabled-server state vanishes silently with no visible diagnostic.

quarantineCorruptFile already established the pattern of writing to process.stderr because the debug logger is gated. The same approach would work here:

Suggested change
debugLogger.error('Error reading extension preferences:', error);
process.stderr.write(
`[warn] Could not read extension preferences at ${this.filePath}: ${
error instanceof Error ? error.message : String(error)
}. Using defaults for this session.\n`,
);
debugLogger.error('Error reading extension preferences:', error);

— qwen3.7-max 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 631e271fb. The transient-read catch now writes a [warn] line to process.stderr (matching the quarantineCorruptFile pattern) before the gated debugLogger.error, so a vanished favorites/scopes state has a visible trail.

}
// A transient read error (permission/too-many-files/…) — the file may be
// valid, so do NOT quarantine it; only a parse failure below does that.
debugLogger.error('Error reading marketplace registry:', 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.

[Suggestion] Same as extensionPreferences.ts — transient read errors produce no stderr output. If the marketplace registry file hits EACCES or EMFILE, the user's source list vanishes silently. Add a process.stderr.write matching the quarantineCorruptFile pattern so the failure has a visible trail.

— qwen3.7-max 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 631e271fb. Added the same process.stderr.write warning to the marketplace-registry transient-read catch in sourceRegistry.ts.

// Transient read errors (EACCES/EMFILE/EISDIR/…) fall through to the
// outer catch, which must NOT quarantine an otherwise-valid file.
debugLogger.error('Corrupt extension preferences:', parseError);
quarantineCorruptFile(this.filePath);

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] Test coverage gaps for the quarantine narrowing. The core behavioral change here — parse errors quarantine, transient read errors do NOT — has no test verification:

  1. No test asserts quarantineCorruptFile actually ran on a corrupt JSON file (i.e., the .corrupted sibling exists after a parse failure).
  2. No test simulates a transient read error (mock readFileSync to throw EACCES) and asserts the file is NOT quarantined.

The same gaps exist in sourceRegistry.test.ts. Without these tests, a future refactor could silently re-introduce the old "quarantine on any error" bug — which would wipe user preferences on a temporary permission error.

— qwen3.7-max 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.

Added in 631e271fb. Both extensionPreferences.test.ts and sourceRegistry.test.ts now assert: (1) a parse failure quarantines to a .corrupted sibling and preserves the original bytes; (2) a transient read error (EISDIR via a directory at the path — same class as EACCES/EMFILE) does NOT quarantine, leaves the file intact, and still warns on stderr. A future "quarantine on any error" regression would now fail these.

await manager.enableExtension(name, SettingScope.User);
} else {
await manager.disableExtension(name, SettingScope.User);
await manager.enableExtension(name, SettingScope.Workspace);

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] Missing rollback when enableExtension(Workspace) fails after disableExtension(User) succeeds. The CLI install.ts (lines 108-120) has an explicit try/catch that re-enables at User scope on Workspace-enable failure, but this UI path does not. If the Workspace enable throws, the extension is left disabled at all scopes — the user sees an error toast but the extension is silently dead.

Suggested change
await manager.enableExtension(name, SettingScope.Workspace);
await manager.disableExtension(name, SettingScope.User);
try {
await manager.enableExtension(name, SettingScope.Workspace);
} catch (enableError) {
try {
await manager.enableExtension(name, SettingScope.User);
} catch {
// Best-effort rollback
}
throw enableError;
}

— qwen3.7-max 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 631e271fb. handleScope now wraps the Workspace enable in a try/catch and rolls the User enable back on failure (mirroring the CLI install.ts pattern), so a failed Workspace enable can no longer leave the extension disabled at every scope. I also moved setExtensionScope to run only after enablement succeeds, so the prefs can't point at a scope the extension isn't actually enabled at.

// workspace-scoped choice, re-scope enablement to this workspace
// only: disable the global enable and enable for the workspace path.
if (scope !== 'user') {
await extensionManager.disableExtension(

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] Same missing rollback pattern: project-scoped install disables User then enables Workspace with no try/catch. Worse than ExtensionActionsView because the outer catch (line 249) logs and swallows the error — the install counts as success while the extension silently doesn't activate.

Suggested change
await extensionManager.disableExtension(
if (scope !== 'user') {
await extensionManager.disableExtension(
ext.name,
SettingScope.User,
);
try {
await extensionManager.enableExtension(
ext.name,
SettingScope.Workspace,
);
} catch (enableError) {
try {
await extensionManager.enableExtension(
ext.name,
SettingScope.User,
);
} catch {
// Best-effort rollback
}
throw enableError;
}
}

— qwen3.7-max 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 631e271fb. The project-scope branch now rolls the User enable back if the Workspace enable throws — important here precisely because (as you noted) the outer catch only logs and the install still reports success, so without the rollback the extension would be silently dead. setExtensionScope is now recorded only after enablement succeeds.

);
const scope = normalizeScope(args.scope);
if (args.scope) {
extensionManager.setExtensionScope(extension.name, scope);

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] setExtensionScope(name, scope) is persisted before the enable/disable try/catch. If Workspace enable fails, the rollback correctly re-enables at User scope, but the scope preference still records 'project'. The Installed tab will show the extension in the "Project level" group even though it's actually enabled at User scope.

Move setExtensionScope to after the enablement succeeds, or roll it back in the catch alongside the enablement rollback.

— qwen3.7-max 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 631e271fb. Moved setExtensionScope(extension.name, scope) to after the enable/disable block. On a Workspace-enable failure the rollback re-enables User scope and the scope preference is never written (defaults to user), so the Installed tab no longer mislabels it as "Project level". Same reorder applied to the two UI install paths.

@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.

Re-reviewed against the current head b5fa71a3f9 (round 4) after the latest push.

Round 4 resolved the previously-open items (verified at HEAD): shared stripAnsiAndControl in textUtils.ts (the sanitizeForError/sanitizeDisplay duplication is closed); quarantineCorruptFile now surfaces on stderr instead of only the gated debug log; quarantine narrowed to genuine parse failures (transient EACCES/EMFILE/… no longer quarantine a valid file); install.ts rollback-failure now surfaced and ref/sha interpolation sanitized; new tests for the gemini symlink-escape branch, corruptFile (rename + byte preservation), and the install rollback (success + double-failure).

🔴 Still open — the one item round 4 did NOT touch: ExtensionActionsView.tsx handleScope still runs disableExtension(User)enableExtension(Workspace) with no rollback (only an error toast at the catch). If the Workspace enable throws (settings write EACCES/EROFS, or a tool-refresh failure), the extension is left disabled at every scope with no recovery — the exact hazard install.ts now has a rollback for. The earlier Critical filed inline at ExtensionActionsView.tsx:229 stands; please mirror the install.ts pattern (wrap the Workspace enable, best-effort re-enable User on failure). My earlier CHANGES_REQUESTED remains until this is addressed.

🟡 New finding (inline): persisted-version ANSI/control-char injection — sanitized in Discover, rendered raw in the Installed tab. See the inline comment on InstalledTab.tsx.

ℹ️ Pre-existing sinks newly exposed by the marketplace install flow (out-of-diff, not blocking — flagged here, not inline):

  • Consent prompt (extensionConsentStringMarkdownDisplay/RenderInline): untrusted plugin name/description/MCP/skill fields reach the terminal without C0/C1 stripping (description only gets stripAnsi, which leaves BEL/BS/CR). The consent dialog is the last gate before install, so a crafted plugin can spoof it. consent.ts is unchanged by this PR, but the marketplace flow is what newly routes attacker-controlled metadata into it.
  • cloneFromGit applies no scheme allowlist to the marketplace-supplied source.url (new git-subdir/url branches). Note: the ext:: RCE vector is blocked by git's default protocol.ext.allow=never — I verified locally (git 2.50.1 → fatal: transport 'ext' not allowed), so this is not RCE. But file:// (local clone) and git:// remain reachable with a marketplace-controlled URL; an explicit scheme allowlist would be reasonable defense-in-depth.

Verdict: Comment — round 4 is solid progress; handleScope rollback is the single remaining blocker.

中文版

已针对 round 4 的当前 head b5fa71a3f9(最新一次 push 之后)重新评审。

Round 4 已修复上一轮遗留项(在 HEAD 上已核实):textUtils.ts 抽出共享的 stripAnsiAndControlsanitizeForError/sanitizeDisplay 的重复实现已收敛);quarantineCorruptFile 现在会写 stderr,不再只走被 QWEN_DEBUG_LOG_FILE 网关挡住的 debug 日志;隔离逻辑收窄为仅在真正 JSON 解析失败时触发(瞬时 EACCES/EMFILE 等不再误隔离有效文件);install.ts 回滚失败已对外提示,ref/sha 插值已做净化;新增了 gemini 符号链接越界分支、corruptFile(重命名 + 字节保全)、install 回滚(成功 + 双重失败)的测试。

🔴 仍未解决 —— round 4 唯一没动的文件: ExtensionActionsView.tsxhandleScope 依旧是 disableExtension(User)enableExtension(Workspace) 两步操作且没有回滚(catch 里只弹一个错误 toast)。若 Workspace 启用步骤抛错(设置写入 EACCES/EROFS,或刷新工具失败),扩展会被留在所有 scope 都禁用的状态且无法自动恢复 —— 正是 install.ts 现在已加回滚来防的那个隐患。之前在 ExtensionActionsView.tsx:229 提的 Critical 依然成立;请照搬 install.ts 的写法(包住 Workspace 启用,失败时尽力把 User 重新启用回来)。在修复前,我之前的 CHANGES_REQUESTED 保持不变。

🟡 新发现(行内评论): 持久化的 version 存在 ANSI/控制字符注入 —— Discover 里做了净化,但 Installed 标签页是原样渲染。详见 InstalledTab.tsx 的行内评论。

ℹ️ 被 marketplace 安装流程新暴露的既有 sink(不在本 PR diff 内、非阻塞 —— 这里一并提示,不单独行内):

  • 同意确认弹窗extensionConsentStringMarkdownDisplay/RenderInline):不可信插件的 name/description/MCP/skill 字段未经 C0/C1 净化就进入终端(description 只过了 stripAnsi,BEL/BS/CR 仍残留)。该弹窗是安装前的最后一道关卡,被构造的插件可以伪造它。consent.ts 本身没改,但 marketplace 流程是把攻击者可控元数据新引入到这里的来源。
  • cloneFromGit 对 marketplace 提供的 source.url 没有任何 scheme 白名单(新增的 git-subdir/url 分支)。说明:ext:: 这个 RCE 向量被 git 默认的 protocol.ext.allow=never 挡住了 —— 本地实测确认(git 2.50.1 → fatal: transport 'ext' not allowed),所以不是 RCE。但 file://(克隆本地仓库)和 git:// 仍可被 marketplace 控制的 URL 触达;加一个显式 scheme 白名单作为纵深防御是合理的。

结论:Comment —— round 4 进展扎实;handleScope 回滚是唯一剩下的阻塞项。

— claude-opus-4-8 via Claude Code /qreview

item.kind === 'mcp'
? t('MCP')
: t('Extension v{{version}}', {
version: item.extension.version,

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] Persisted version reaches the terminal unsanitized — ANSI/control-char injection on the Installed tab.

convertClaudeToQwenConfig persists a marketplace plugin's version verbatim (claude-converter.ts:417), and on load only name is validated (validateName) — version is neither validated nor sanitized. The Discover tab scrubs these fields via sanitizeDisplay/stripAnsiAndControl, but here (and at PluginDetailView.tsx:128) the persisted version is rendered raw in an Ink <Text>.

A marketplace plugin with "name":"safe-name" (passes validateName) and "version":"1.0.0\u001b[2J\u001b[H…" injects escape sequences (screen-clear, cursor moves, a forged ✓ verified banner) on every Installed-tab render after install. Same class as the Discover-side ANSI injection fixed earlier in this PR — just on the new persisted/Installed render path that the Discover sanitization doesn't cover.

Suggested change
version: item.extension.version,
version: stripUnsafeCharacters(item.extension.version ?? ''),

Also add the import (as SourcesTab.tsx:25 already does): import { stripUnsafeCharacters } from '../../../utils/textUtils.js';, and apply the same wrap at PluginDetailView.tsx:128. Sanitizing at the persistence boundary in convertClaudeToQwenConfig would fix all sinks at once and also defend already-installed extensions.

— claude-opus-4-8 via Claude Code /qreview

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 631e271fb. Wrapped the persisted version with stripUnsafeCharacters at both raw sinks — InstalledTab.tsx (the Extension v{{version}} badge) and PluginDetailView.tsx (the Version row). I chose render-time sanitization over the persistence boundary because it also defends already-installed extensions whose raw version is already on disk. (The third version render in steps/ExtensionDetailStep.tsx is the retired step architecture — not mounted by any live tab/view, so left untouched.)

…ion sanitization, visible read warnings

- Critical: the UI scope-change (ExtensionActionsView) and project-scope
  install (DiscoverTab) disabled User then enabled Workspace with no
  rollback — a failed Workspace enable left the extension disabled at all
  scopes (silently dead, and in DiscoverTab the outer catch swallowed it so
  the install still reported success). Mirror the CLI install.ts pattern:
  roll the User enable back on failure.
- Persist the scope preference only AFTER enablement succeeds (install.ts +
  both UI paths), so a rolled-back enable can't leave prefs pointing at a
  scope the extension isn't actually enabled at (Installed tab mislabel).
- Security: the persisted 'version' is rendered raw on the Installed tab and
  PluginDetailView — only 'name' is validated on load, so a marketplace
  plugin could inject ANSI/control sequences post-install on every render.
  Scrub via stripUnsafeCharacters at both sinks (covers already-installed
  extensions; the Discover-side sanitization doesn't reach this path).
- Transient read errors in extensionPreferences/sourceRegistry only logged
  via the gated debugLogger, so a user's favorites/scopes/sources could
  vanish with no trail. Add an stderr warning matching quarantineCorruptFile.
- Tests: assert quarantine runs on a parse error (.corrupted sibling) and
  does NOT run on a transient read error (EISDIR), for both stores.
@BZ-D

BZ-D commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @wenshao and the /review bot for round 5 🙏. Pushed 631e271fb addressing every thread; replied inline on each.

✅ Critical — scope-change rollback (parity with CLI install.ts)

  • ExtensionActionsView (scope change) and DiscoverTab (project-scope install) disabled User then enabled Workspace with no rollback — a failed Workspace enable left the extension disabled at every scope (silently dead; in DiscoverTab the outer catch swallowed it so the install still reported success). Both now roll the User enable back on failure.

✅ Correctness — scope-preference ordering

  • setExtensionScope is now persisted only after enablement succeeds (install.ts + both UI paths). A rolled-back enable no longer leaves prefs claiming project while the extension is actually enabled at User scope (Installed-tab mislabel).

✅ Security — persisted version ANSI injection on the Installed tab

  • version is stored verbatim by the converter and only name is validated on load, so a marketplace plugin could inject escape sequences on every Installed-tab / detail render after install. Now scrubbed via stripUnsafeCharacters at both sinks (InstalledTab, PluginDetailView). Chose render-time over the persistence boundary so it also defends already-installed extensions. (Third render site steps/ExtensionDetailStep.tsx is the retired step path — not mounted.)

✅ Robustness — visible diagnostics on transient read errors

  • extensionPreferences / sourceRegistry transient-read catches now write a [warn] line to stderr (matching quarantineCorruptFile) instead of only the gated debugLogger, so a vanished favorites/scopes/sources state has a trail.

✅ Tests — quarantine narrowing locked in

  • Both stores now assert a parse error quarantines (.corrupted sibling, original bytes preserved) and a transient read error (EISDIR) does NOT quarantine and leaves the file intact. Guards against a future "quarantine on any error" regression.

Validation: core extensionPreferences 11 ✅ + sourceRegistry 18 ✅; cli install/sources/ExtensionsManagerDialog 47 ✅ (run with NO_COLOR — the truecolor snapshot diffs in steps/* and list.test.ts reproduce on a clean main checkout too, i.e. local-TTY-only, not from this PR). tsc, eslint, prettier clean on all changed files.

Also note: the 8 stray test review comments from 06-15 appear to be tooling test-posts (no content) — leaving them for you to clear unless you'd like me to do anything with them.

Re-requesting review when you have a moment 🙏.

中文

第 5 轮意见已在 631e271fb 全部处理,并逐条 inline 回复:

  • Critical(scope 切换回滚):ExtensionActionsView(改作用域)与 DiscoverTab(项目级安装)此前 disable User 后 enable Workspace 无回滚,Workspace enable 失败会让扩展在所有作用域被禁用(静默失效;DiscoverTab 外层 catch 还会吞掉错误使安装误报成功)。现两处均在失败时回滚 User enable。
  • 正确性(scope 偏好顺序):setExtensionScope 改为 enable 成功后才写入(install.ts + 两个 UI 路径),回滚后不再误标 "Project level"。
  • 安全(Installed 端 version 注入):持久化的 version 原样存储、加载时只校验 name,故安装后每次渲染都可能注入转义序列。现在 InstalledTab/PluginDetailView 两个 sink 用 stripUnsafeCharacters 清洗;选渲染端是为了同时防护已安装扩展。第三处 steps/ExtensionDetailStep.tsx 是退役 step 架构,未挂载。
  • 健壮性(瞬时读错误可见):extensionPreferences/sourceRegistry 的瞬时读错误改为额外写 stderr [warn](与 quarantineCorruptFile 一致)。
  • 测试:两个 store 均断言「解析错误 → quarantine 到 .corrupted」「瞬时错误(EISDIR)→ 不 quarantine、文件完好」,防止未来回退成"任何错误都 quarantine"。

验证:core 29 ✅、cli 47 ✅(NO_COLOR 运行;steps/*list.test.ts 的颜色快照差异在干净 main 上同样复现,属本地 TTY 颜色问题,非本 PR)。tsc/eslint/prettier 全绿。另:06-15 那 8 条 test 评论疑似工具测试贴(无内容),先留着,需要的话我再处理。

@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.

All findings from my previous review are resolved in round 5 (631e271fba) — verified at HEAD. ✅

  • Scope-change rollback (Critical)ExtensionActionsView.handleScope now wraps the Workspace enable in try/catch and best-effort re-enables User on failure, and records the scope preference only after enablement succeeds. The sibling DiscoverTab install path got the same rollback, and install.ts was reordered to persist scope only after enablement. Correct.
  • Persisted-version ANSI injection (Suggestion) — both sinks (InstalledTab.tsx, PluginDetailView.tsx) now wrap version in stripUnsafeCharacters(… ?? ''), with the import added. Correct.
  • Transient-read silent empty stateextensionPreferences.ts and sourceRegistry.ts now surface read failures on stderr. New tests cover parse-error quarantine vs. transient-read (EISDIR) non-quarantine, plus a discoverPlugins ANSI-stripping assertion. Correct.

This feature has been through a thorough multi-round security review (path/symlink confinement, ANSI sanitization at every marketplace-metadata boundary, fetch size/time caps, corrupt-file quarantine, non-atomic scope rollbacks) and the in-diff code is now clean. CI green (30 checks).

Recommended follow-up (non-blocking, pre-existing & out-of-diff): the install consent prompt (extensionConsentStringMarkdownDisplay/RenderInline) still renders untrusted plugin description/name/MCP/skill fields without C0/C1 stripping (description only gets stripAnsi). It is pre-existing shared code that this PR newly routes marketplace metadata into; worth sanitizing in a follow-up so the consent dialog can't be spoofed. Not a blocker for this PR.

Verdict: Approve.

中文版

上一轮 review 提的问题在 round 5(631e271fba)里都已修复,已在 HEAD 上核实。✅

  • scope 切换回滚(Critical) —— ExtensionActionsView.handleScope 现在用 try/catch 包住 Workspace 启用,失败时尽力把 User 重新启用回来,并且只在启用成功后才记录 scope 偏好。同源的 DiscoverTab 安装路径加了相同的回滚,install.ts 也调整为启用成功后再持久化 scope。修法正确。
  • 持久化 version 的 ANSI 注入(Suggestion) —— 两个渲染点(InstalledTab.tsxPluginDetailView.tsx)现在都用 stripUnsafeCharacters(… ?? '') 包住 version,并补了 import。正确。
  • 瞬时读失败导致静默空状态 —— extensionPreferences.tssourceRegistry.ts 现在会把读失败写到 stderr。新增测试覆盖了"解析失败→隔离"与"瞬时读失败(EISDIR)→不隔离"两条分支,外加 discoverPlugins 的 ANSI 净化断言。正确。

这个特性经过了多轮严格的安全 review(路径/符号链接约束、对所有 marketplace 元数据边界做 ANSI 净化、fetch 大小/超时上限、损坏文件隔离、非原子 scope 回滚),diff 内的代码现在是干净的。CI 全绿(30 项)。

建议的后续项(不阻塞、属既有代码且不在本 diff 内): 安装同意确认弹窗extensionConsentStringMarkdownDisplay/RenderInline)仍然把不可信插件的 description/name/MCP/skill 字段未经 C0/C1 净化就渲染(description 只过了 stripAnsi)。这是本 PR 新引流 marketplace 元数据进入的既有共享代码;建议在后续 PR 里净化,避免同意弹窗被伪造。不阻塞本 PR。

结论:Approve

— claude-opus-4-8 via Claude Code /qreview

@BZ-D
BZ-D dismissed qwen-code-ci-bot’s stale review June 20, 2026 03:57

All critical issues resolved

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

🔄 [UPDATED] Superseded by the round-6 re-verification at head 158002c773. The pass below covered head 631e271fba.


✅ Re-verification at new head 631e271fba (maintainer) — recommendation stands: merge

Supersedes my earlier verification (which covered head 61a69e2729). The author has since pushed 4 commits (review rounds 4-5); I re-built from source on macOS and re-verified the delta. Headline: the rounds-4/5 changes are all review-driven hardening, and they close the one gap my earlier report flaggedsanitizeDisplay() now has direct, non-vacuous unit coverage.

Re-check (delta 61a69e2729..631e271fba, +483/-50, 17 files) Result
npm ci + npm run build (macOS) ✅ exit 0
Changed core suites (9 files) 276 passed
Changed CLI suites (3 files) 47 passed
Mutation test on the new ANSI/control tests ✅ disabling the sanitizer fails exactly them → non-vacuous
Real-TUI: shared-sanitizer refactor + round-5 version sanitization ✅ still strips ANSI/OSC at runtime
CI Lint ✅ · 3× Test + CodeQL still pending (recent push)

1. What changed since my last report

Four commits, all hardening in response to review:

631e271fba  round 5 — scope-change rollback, version sanitization, visible read warnings
a2d63d7807  test — assert discoverPlugins strips ANSI/control chars from display fields   <- closes my flagged gap
b5fa71a3f9  round 4 — shared sanitizer, narrowed quarantine, visible warnings, tests
a7b2df2fb4  test — normalize realpathSync mock so the gemini guard passes on Windows
  • Shared sanitizer: the per-file stripVTControlCharacters + C0/C1 copies are unified into one exported stripAnsiAndControl() in utils/textUtils.ts; sourceRegistry, claude-converter, and workflow-orchestrator now all delegate to it. The orchestrator's old local regex (\x00\x1f + \x7f) actually missed the C1 range (\x80\x9f); the shared one fixes that — a real (small) bug closed as a side effect of de-duplication.
  • Narrowed quarantine (data-loss fix): SourceRegistryStore.read() and ExtensionPreferencesStore.read() previously moved the file aside on any read error. They now quarantine only on a JSON parse failure; a transient read error (EACCES / EMFILE / …) keeps the file and emits a visible stderr warning (since debugLogger.error is gated behind QWEN_DEBUG_LOG_FILE, which is unset for almost everyone → previously a silent data-vanish).
  • Round 5: version is now sanitized alongside the other display fields; the converter error path also sanitizes ref ?? sha ?? 'HEAD' (was interpolated raw).

2. Delta tests (macOS)

core (9): sourceRegistry 18 · textUtils 25 · corruptFile 2 · extensionPreferences 11
          gemini-converter 11 · claude-converter 35 · marketplace 23 · extensionManager 50
          workflow-orchestrator 101                                         -> 276 passed
cli  (3): ExtensionsManagerDialog 23 · extensions/install · extensions/sources 11 -> 47 passed

3. The flagged gap is now genuinely closed (mutation test) ⭐

The new discoverPlugins > strips ANSI/control sequences from untrusted display fields test even notes "Payload mirrors the PoC from the PR #4850 verification report" — it feeds an ANSI/OSC/BEL payload through every display field and asserts each comes out clean.

To confirm it isn't vacuous, I disabled the shared sanitizer (stripAnsiAndControlreturn text) and re-ran:

× discoverPlugins > strips ANSI/control sequences from untrusted display fields
   -> expected 'Hostile<ESC>[2K' to be 'Hostile'
× stripAnsiAndControl > strips ANSI/VT escape sequences
× stripAnsiAndControl > strips OSC 8 hyperlink sequences but keeps the link text
× stripAnsiAndControl > removes residual C0 control chars and DEL
× stripAnsiAndControl > removes C1 control chars (the range a drifted local copy missed)
Failed Tests 5

Exactly the sanitization tests fail (including one that specifically guards the C1 range the orchestrator regex used to miss), and nothing else → the coverage is real. Restored → all green again.

4. Real-TUI re-confirmation (the refactor didn't regress runtime behavior)

Same hostile-marketplace PoC, real built binary, isolated $HOME. After the shared-sanitizer refactor and the round-5 version sanitization, the pre-consent Discover view and the detail view still neutralize everything:

Discover list :  name -> "evil-plugin"      desc -> "Totally safe — trust me"
Detail view   :  Version: 9.9.9            By: mallory      homepage -> https://evil.com
byte-level (capture -e):  PWNED 0 · OSC ]0; 0 · cursor-up [1A 0 · clear-line [2K 0 · blink [5m 0
terminal pane title    :  "Qwen - project"   (NOT "PWNED" — OSC title-injection neutralized)

The three tabs (Installed grouping + nested MCP status, Discover, Sources) all still render and navigate correctly.

Verdict

The rounds-4/5 delta is clean, well-reasoned hardening that resolves the only open item from my prior pass. Build green, all changed suites green, the new security tests are non-vacuous, and runtime sanitization is intact. Recommendation stands: merge once the (currently pending) CI Test matrix goes green. Note the macOS Test job may again trip the unrelated cronScheduler durable-ownership timing flake described in my earlier comment — that remains a false signal, not this PR.

🇨🇳 中文版(点击展开)

✅ 在新 head 631e271fba 上的复验(维护者)—— 结论不变:建议合并

本评论取代我此前的验证(那次针对 head 61a69e2729)。作者之后又推了 4 个 commit(review 第 4、5 轮);我在 macOS 上从源码重新构建并复验了增量。要点:第 4/5 轮全是 review 驱动的加固,而且堵上了我上次报告标注的唯一缺口 —— sanitizeDisplay() 现在有了直接、非空泛的单测覆盖。

复检项(增量 61a69e2729..631e271fba,+483/-50,17 文件) 结果
npm ci + npm run build(macOS) ✅ exit 0
改动的 core 套件(9 文件) 276 通过
改动的 CLI 套件(3 文件) 47 通过
新增 ANSI/控制字符测试的 mutation test ✅ 禁用 sanitizer 后恰好这些失败 → 非空泛
实机 TUI:共享 sanitizer 重构 + 第 5 轮 version 净化 ✅ 运行时仍剥离 ANSI/OSC
CI Lint ✅ · 三平台 Test + CodeQL 仍 pending(刚推送)

1. 自上次报告以来改了什么

四个 commit,全是回应 review 的加固:

631e271fba  第 5 轮 — scope 变更回滚、version 净化、可见读取警告
a2d63d7807  测试 — 断言 discoverPlugins 从显示字段剥离 ANSI/控制字符   <- 堵上我标注的缺口
b5fa71a3f9  第 4 轮 — 共享 sanitizer、收窄 quarantine、可见警告、测试
a7b2df2fb4  测试 — 规范化 realpathSync mock 让 gemini guard 在 Windows 通过
  • 共享 sanitizer: 各文件里 stripVTControlCharacters + C0/C1 的副本被统一成 utils/textUtils.ts 里一个导出的 stripAnsiAndControl();sourceRegistryclaude-converterworkflow-orchestrator 现在都委托给它。orchestrator 旧的本地正则(\x00\x1f + \x7f)其实漏了 C1 范围(\x80\x9f),共享版顺手修掉了 —— 去重附带修了个(小)真实 bug。
  • 收窄 quarantine(数据丢失修复): SourceRegistryStore.read()ExtensionPreferencesStore.read() 之前对任何读错误都把文件挪走。现在在 JSON 解析失败时 quarantine;瞬时读错误(EACCES / EMFILE / …)保留文件并打可见的 stderr 警告(因为 debugLogger.errorQWEN_DEBUG_LOG_FILE 门控,几乎所有人都没设 → 之前是静默丢数据)。
  • 第 5 轮: version 现在和其他显示字段一起被净化;转换器错误路径也净化了 ref ?? sha ?? 'HEAD'(之前是裸插值)。

2. 增量测试(macOS)

core (9): sourceRegistry 18 · textUtils 25 · corruptFile 2 · extensionPreferences 11
          gemini-converter 11 · claude-converter 35 · marketplace 23 · extensionManager 50
          workflow-orchestrator 101                                         -> 276 通过
cli  (3): ExtensionsManagerDialog 23 · extensions/install · extensions/sources 11 -> 47 通过

3. 标注的缺口现已真正堵上(mutation test)⭐

新测试 discoverPlugins > strips ANSI/control sequences from untrusted display fields 的注释甚至写着 "Payload mirrors the PoC from the PR #4850 verification report" —— 它把一份 ANSI/OSC/BEL 载荷喂过每个显示字段,断言每个都干净输出。

为确认它非空泛,我把共享 sanitizer 禁用(stripAnsiAndControlreturn text)后重跑:

× discoverPlugins > strips ANSI/control sequences from untrusted display fields
   -> expected 'Hostile<ESC>[2K' to be 'Hostile'
× stripAnsiAndControl > strips ANSI/VT escape sequences
× stripAnsiAndControl > strips OSC 8 hyperlink sequences but keeps the link text
× stripAnsiAndControl > removes residual C0 control chars and DEL
× stripAnsiAndControl > removes C1 control chars (the range a drifted local copy missed)
Failed Tests 5

恰好这些净化测试失败(其中一个专门守护 orchestrator 正则曾漏掉的 C1 范围),其余无碍 → 覆盖是真实的。还原后重新全绿。

4. 实机 TUI 复核(重构没有破坏运行时行为)

同一份恶意 marketplace PoC、真实构建二进制、隔离 $HOME。在共享 sanitizer 重构以及第 5 轮 version 净化之后,安装前的 Discover 视图与详情视图仍然消解了所有载荷:

Discover 列表 :  name -> "evil-plugin"      desc -> "Totally safe — trust me"
详情视图      :  Version: 9.9.9            By: mallory      homepage -> https://evil.com
字节级(capture -e):  PWNED 0 · OSC ]0; 0 · 光标上移 [1A 0 · 清行 [2K 0 · 闪烁 [5m 0
终端 pane 标题      :  "Qwen - project"   (不是 "PWNED" —— OSC 改标题注入被消解)

三个 tab(Installed 分组 + 嵌套 MCP 状态、Discover、Sources)仍正确渲染和导航。

结论

第 4/5 轮的增量是干净、有据的加固,解决了我上一轮唯一的遗留项。构建绿、所有改动套件绿、新增安全测试非空泛、运行时净化完好。结论不变:建议合并 —— 待(当前 pending 的)CI Test 矩阵转绿即可。注意 macOS Test job 可能再次踩到我上条评论里描述的与本 PR 无关的 cronScheduler durable-ownership 计时 flaky —— 那仍是误报,不是本 PR。

Maintainer re-verification · head 631e271fba (prev 61a69e2729) · macOS 26.5 / Node 22.22.2.

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

{marketplaceFilter ? (
<Text color={theme.text.secondary}>
{t(' · {{marketplace}} (Tab to clear)', {
marketplace: marketplaceFilter,

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] ANSI/terminal escape injection via marketplaceFilter. When the user clicks "Browse extensions" on the Sources tab, the raw marketplace name (detailSource.name from ExtensionSource) is passed as marketplaceFilter and rendered unsanitized here. The marketplace name originates from the remote marketplace.json's config.name field (untrusted), stored raw by addSource in extensionManager.ts.

The prior ANSI fix (round 1) sanitized plugin metadata (DiscoveredPlugin.marketplaceName) at the sourceRegistry.ts boundary, but this is a separate injection vector: the marketplace's own name, passed cross-component through onBrowse, bypasses the sanitizeDisplay applied to plugin fields.

A hostile marketplace can embed ANSI/OSC escape sequences in its name field. When browsed from the Sources tab, the sequences reach the terminal unsanitized — potentially spoofing UI elements, clearing lines, or moving the cursor.

Suggested change
marketplace: marketplaceFilter,
marketplace: stripUnsafeCharacters(marketplaceFilter),

Alternatively, sanitize at the handleBrowseSource callback or in addSource so the raw name is never persisted with escape sequences.

— qwen3.7-max 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 158002c77. Sanitized the marketplace name in handleBrowseSource (ExtensionsManagerDialog) via stripUnsafeCharacters before it becomes the Discover filter. I chose that boundary over the render site because the filter is also compared against the already-sanitized DiscoveredPlugin.marketplaceName — so sanitizing here blocks the ANSI injection AND keeps the filter comparison matching (a raw control-char name would otherwise match zero plugins).

</Text>
{marketplaceFilter ? (
<Text color={theme.text.secondary}>
{t(' · {{marketplace}} (Tab to clear)', {

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] The hint says (Tab to clear) but the DiscoverTab's useKeypress handler has no Tab key handler. Pressing Tab triggers the parent's cycleTab, which switches to the next tab rather than clearing the filter in place. Worse, the catch-all key.sequence >= ' ' condition at the list keypress handler would append a literal \t character to the search query instead.

Suggested change
{t(' · {{marketplace}} (Tab to clear)', {
{t(' · {{marketplace}} (Esc to clear)', {

Or add a dedicated key handler (e.g., Esc or a specific key combo) that calls an onClearFilter callback to set marketplaceFilter to null without switching tabs.

— qwen3.7-max 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 158002c77. Rather than change the hint, I made it true: on the Discover tab with an active marketplace filter, Tab now clears the filter in place (revealing all extensions) instead of cycling tabs. This lives in the parent ExtensionsManagerDialog Tab handler — useKeypress broadcasts to all subscribers with no propagation control, so handling Tab in the child would race the parent's cycleTab; doing it in the parent (which owns both the filter state and the Tab handler) is conflict-free. (Note: a bare Tab is \x09 < 0x20`, so the list catch-all never appended it to the query — but the in-place clear is the real fix.)

SettingScope.User,
);
} catch {
// Best-effort rollback.

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] Rollback failure is silently swallowed. The CLI's install.ts (line 122-128) surfaces rollback failures with writeStderrLine, warning the user the extension may be disabled at all scopes. This UI code silently swallows the rollback error with a bare catch.

Scenario: install succeeds → disableExtension(User) lands → enableExtension(Workspace) fails → rollback enableExtension(User) also fails. The user sees either a success message or a generic error about the workspace-enable failure, but nothing about the rollback failure that left the extension disabled at every scope.

Suggested change
// Best-effort rollback.
} catch (rollbackError) {
debugLogger.error('Rollback failed:', rollbackError);
errors.push(`${plugin.name}: install succeeded but scope rollback failed — the extension may be disabled at all scopes: ${getErrorMessage(rollbackError)}`);
}

— qwen3.7-max 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 158002c77. The inner rollback catch now captures the failure: it debug-logs and pushes a descriptive entry to the batch errors[] (new i18n key) so the user is told the extension may be disabled at all scopes and to re-enable it from the Installed tab, instead of the install silently reporting success.

try {
await manager.enableExtension(name, SettingScope.User);
} catch {
// Best-effort rollback.

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] Same rollback swallowing as DiscoverTab.tsx:258. The CLI's install.ts (lines 122-128) surfaces rollback failures with writeStderrLine, but this UI code silently swallows them. If the rollback fails, the extension ends up disabled at all scopes while the user only sees the original enable failure in the error toast.

Consider surfacing the rollback failure so the user knows to manually re-enable the extension:

Suggested change
// Best-effort rollback.
} catch (rollbackError) {
debugLogger.error('Rollback failed:', rollbackError);
throw new Error(`${getErrorMessage(error)}\n\nRollback also failed — "${name}" may be disabled at all scopes. Please enable it manually from the Installed tab.`);
}

— qwen3.7-max 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 158002c77. Same treatment as DiscoverTab: when the rollback enableExtension(User) also fails, handleScope now throws an explicit, translated error telling the user the extension may be disabled at all scopes and to re-enable it from the Installed tab — rather than surfacing only the bare workspace-enable error.

);
}

/** Max time to wait for a single marketplace network request. */

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] The new security guards (wall-clock timeout + body-size cap) have no test coverage. These are the exact threat-model mitigations described in the code comments — a slow/trickling server and a hostile server streaming unbounded data — yet nothing verifies they actually work.

Add two tests:

  1. Mock https.get with a server that never sends end — assert the promise resolves to null within MARKETPLACE_FETCH_TIMEOUT_MS (use vi.useFakeTimers()).
  2. Mock https.get with a server that streams >10 MB in chunks — assert req.destroy() is called and the result is null.

— qwen3.7-max 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.

Added in 158002c77 (marketplace.test.ts). Two tests drive fetchUrl through a single direct-JSON fetch (https://example.com/marketplace.json): (1) a stalled server that connects but never emits data/end → vi.useFakeTimers() + advanceTimersByTimeAsync past the 10s wall-clock deadline asserts it resolves to null and req.destroy() is called; (2) a server streaming an 11 MB chunk asserts the body-size cap aborts mid-stream (req.destroy() called, result null).

return `${marketplace.source}:${plugin.name}`;
}
const src = plugin.source;
if (typeof src === 'string') {

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] The local-path injection guard for remote (http) marketplaces — rejecting source strings that are absolute paths, start with ., or start with ~ — has no test coverage. This security guard prevents a hostile remote marketplace from pointing the installer at local filesystem paths.

Since resolveInstallSource is module-private, test it via discoverPlugins:

it('rejects local-path sources from a remote (http) marketplace', async () => {
  vi.mocked(loadMarketplaceConfigFromSource).mockResolvedValue(
    config('Remote', [
      { name: 'evil', version: '1.0.0', source: '/etc/passwd' },
      { name: 'rel', version: '1.0.0', source: '../../secret' },
      { name: 'home', version: '1.0.0', source: '~/.ssh/id_rsa' },
    ]),
  );
  const discovered = await discoverPlugins(
    [{ name: 'Remote', source: 'https://x/m.json', type: 'http' }],
    new Set(),
  );
  expect(discovered.find(p => p.name === 'evil')!.installSource).toBe('evil');
});

— qwen3.7-max 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.

Added in 158002c77 (sourceRegistry.test.ts). A new discoverPlugins test feeds a remote (http) marketplace plugins with /etc/passwd, ../../secret, and ~/.ssh/id_rsa sources (both bare-string and { source: 'url' } forms) and asserts each falls back to the bare plugin name (no install redirect), while a genuine https:// URL is preserved.

// workspace-scoped choice, re-scope enablement to this workspace
// only: disable the global enable and enable for the workspace path.
if (scope !== 'user') {
await extensionManager.disableExtension(

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] Missing scope-change rollback guard.

When scope !== 'user', this disables at User then enables at Workspace. If the Workspace enable fails, the outer catch only logs — the extension is left disabled at all scopes, but the user already saw "Installed N extension(s)."

install.ts:97-130 handles this correctly with an explicit rollback:

Suggested change
await extensionManager.disableExtension(
if (scope !== 'user') {
await extensionManager.disableExtension(
ext.name,
SettingScope.User,
);
try {
await extensionManager.enableExtension(
ext.name,
SettingScope.Workspace,
);
} catch (enableError) {
try {
await extensionManager.enableExtension(
ext.name,
SettingScope.User,
);
} catch {
// rollback also failed
}
throw enableError;
}
}

— qwen3.7-max 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.

Already addressed in 631e271fb (round 5) — this thread targets a pre-round-5 snapshot. The Discover batch-install path already has exactly this disableExtension(User)try enableExtension(Workspace) → rollback-to-User pattern. Round 6 (158002c77) went further and now also surfaces a rollback-failure to the user (see the related thread on line ~258).

if (newScope === 'user') {
await manager.enableExtension(name, SettingScope.User);
} else {
await manager.disableExtension(name, SettingScope.User);

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] Same scope-change rollback gap as DiscoverTab.tsx:239.

disableExtension(User) runs before enableExtension(Workspace). If the workspace enable throws, the catch at line 243 shows an error toast but does not re-enable at User scope. The extension is left disabled everywhere, and the scope preference metadata says "project" but no enablement exists.

Mirror the install.ts rollback pattern:

Suggested change
await manager.disableExtension(name, SettingScope.User);
await manager.disableExtension(name, SettingScope.User);
try {
await manager.enableExtension(name, SettingScope.Workspace);
} catch (enableError) {
try {
await manager.enableExtension(name, SettingScope.User);
} catch {
// rollback also failed
}
throw enableError;
}

— qwen3.7-max 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.

Already addressed in 631e271fb (round 5) — pre-round-5 snapshot. handleScope in ExtensionActionsView already rolls the User enable back when the Workspace enable fails, and records the scope preference only after enablement succeeds. Round 6 (158002c77) additionally surfaces the case where the rollback itself fails.

if (src && src.source === 'github') {
return `${src.repo}:${plugin.name}`;
}
if (src && src.source === 'url') {

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] {source: 'url'} branch bypasses the local-path guard applied to string sources.

The string-source branch (lines 168-175) explicitly rejects local filesystem paths from remote marketplaces (isAbsolute, startsWith('.'), startsWith('~')). But this branch returns src.url without any validation, allowing a hostile http-type marketplace to redirect the installer at arbitrary local paths (e.g. /home/victim/.ssh or ../../etc).

Apply the same local-path guard:

Suggested change
if (src && src.source === 'url') {
if (src && src.source === 'url') {
if (
typeof src.url === 'string' &&
(path.isAbsolute(src.url) || src.url.startsWith('.') || src.url.startsWith('~'))
) {
debugLogger.warn(
`Ignoring local path source "${src.url}" from remote marketplace "${marketplace.source}".`,
);
return plugin.name;
}
return src.url;
}

— qwen3.7-max 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 158002c77. The { source: 'url' } branch in resolveInstallSource now applies the same local-path guard as the string branch (rejecting absolute / .-prefixed / ~-prefixed values and falling back to the plugin name), so a remote http marketplace can no longer redirect the installer at a local path via the structured form. Covered by the new sourceRegistry.test.ts test.

return pluginDir;
}

if (source.source === 'git-subdir') {

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] The git-subdir source type branch (~45 lines) has no test coverage.

This branch handles cloning a repo, pinning to a ref/sha, validating subdirectory confinement (realPathWithin), and guarding against symlink escapes. None of these security-critical code paths — the happy path, path-escape rejection, absolute-path rejection, missing-subdir error, or symlink-escape guard — are tested.

A regression could silently break path confinement, allowing a hostile marketplace entry with path: '../../.ssh' to escape the clone root.

Suggested tests in claude-converter.test.ts:

  1. Valid git-subdir source with a real subdirectory
  2. A path that escapes the repo root (e.g. ../../etc)
  3. An absolute path
  4. A subdir that is a symlink pointing outside the clone

— qwen3.7-max 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.

Added in 158002c77 (claude-converter.test.ts). A new git-subdir suite mocks cloneFromGit to populate the clone dir against a real fs and covers: the happy path (clone + subdir returned, asserting the immutable sha is preferred over the named ref), path-escape (../../etc) rejection, absolute-path rejection, missing-subdir rejection, and the symlink-escape guard (realPathWithin).

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @BZ-D! (Re-run at head 158002c773 — round 6)

Template looks good ✓ — all required sections present (What, Why, Reviewer Test Plan, Evidence, Risk & Scope, Linked Issues).

On direction: This is squarely on the extensions/marketplace roadmap. CHANGELOG shows active investment in the extension system (extension: add description field, cli: bundle extension examples, core: support .toml command files in extension command discovery, cli: implement --list-extensions flag handler). Turning /extensions from a read-only list into a full lifecycle manager is the natural next step — and reusing the existing /mcp dialog components keeps the TUI consistent. Clearly aligned.

On approach: At 53 files / +8616 lines, this is a large PR. But the changes are cohesive: every file serves the extensions management feature. The MCP component modifications are refactoring for reuse (extracting shared components), not scope creep. The security hardening layer (ANSI sanitization via shared stripAnsiAndControl, fetch timeouts + size caps, homepage scheme checks, path-traversal confinement) is the right response to a feature that now fetches and renders untrusted third-party marketplace metadata. The scope-change rollback pattern (both CLI and TUI paths) addresses a real two-step consistency risk. Full i18n parity (en/zh/zh-TW) is appropriate for the project.

Round 6 (+302/-8, 10 files) is a focused security and data-integrity hardening pass: closes a real { source: 'url' } path-traversal bypass, surfaces a previously-silent rollback-failure, and adds +235 lines of non-vacuous security tests. Clean.

Moving on to code review. 🔍

中文说明

感谢贡献,@BZ-D!(在 head 158002c773 — 第 6 轮 — 重新审查)

模板完整 ✓ — 所有必填章节齐全。

方向: 完全契合扩展/市场路线图。CHANGELOG 显示项目在扩展系统上的持续投入。将 /extensions 从只读列表升级为完整生命周期管理器是自然的下一步——复用现有 /mcp 对话框组件保持了 TUI 一致性。方向明确对齐。

方案: 53 个文件 / +8616 行,PR 体量较大。但改动内聚:每个文件都服务于扩展管理功能。MCP 组件修改是为了复用的重构,不是范围蔓延。安全加固层(通过共享 stripAnsiAndControl 做 ANSI 净化、fetch 超时+大小上限、homepage scheme 检查、路径穿越限制)是对新增的第三方市场元数据渲染的正确回应。scope 变更回滚模式(CLI 和 TUI 两条路径)解决了真实的两步一致性风险。完整 i18n 三语支持合理。

第 6 轮(+302/-8,10 文件)是聚焦的安全和数据完整性加固:堵上了真实的 { source: 'url' } 路径穿越绕过,把此前静默的回滚失败显式化,新增 +235 行非空泛安全测试。干净。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Code Review (re-run at head 158002c773)

Independent proposal (before reading the round-6 diff): for a round addressing review feedback on a marketplace manager, I would expect: (1) close the { source: 'url' } local-path bypass with the same guard the string-source branch already applies, (2) surface rollback failures instead of silently swallowing them, (3) sanitize the marketplace name before it becomes a Discover filter value, (4) add tests that would fail if the guard were removed.

Comparison with the diff: Round 6 matches all four expectations exactly.

  • url-source guard (sourceRegistry.ts:182-196): same path.isAbsolute / startsWith('.') / startsWith('~') check as the string branch, with debugLogger.warn and fallback to plugin.name. Clean.
  • Rollback-failure surfacing (DiscoverTab.tsx:257-268, ExtensionActionsView.tsx:236-247): both paths now throw/surface the rollback error with an explicit i18n'd message telling the user the extension may be disabled at all scopes. The ExtensionActionsView re-throws with the rollback error (not the original enable error) — correct, since the rollback failure is the more severe condition.
  • Marketplace-name sanitization (ExtensionsManagerDialog.tsx:114-119): stripUnsafeCharacters applied in handleBrowseSource before the value becomes both the Discover filter and the hint render. The comment explains why both paths need it.
  • Tab-to-clear behavior (ExtensionsManagerDialog.tsx:135-142): Tab now clears the filter on Discover when active instead of cycling tabs, matching the (Tab to clear) hint. Small UX fix, correctly scoped.

No critical blockers found. The round-6 changes are guard and error-path logic — no rendering or data-flow changes.

Test Results

Unit tests — all pass on the PR branch:

Suite Tests Status
sourceRegistry.test.ts 19
marketplace.test.ts 25
claude-converter.test.ts 40
extensionPreferences.test.ts 11
extensionManager.test.ts 50
textUtils.test.ts 25
ExtensionsManagerDialog.test.tsx 23
ExtensionListStep.test.tsx 5
ActionSelectionStep.test.tsx 5
CLI extensions commands (12 suites) 122
Total 325

Typecheck — clean on all packages.

CLI command verification (tmux capture-pane):

$ qwen extensions sources list
No marketplace sources added yet.

$ qwen extensions list
No extensions installed.

$ qwen extensions sources --help
Commands:
  qwen extensions sources add <source>   Adds a marketplace source (Claude format).
  qwen extensions sources remove <name>  Removes a marketplace source.
  qwen extensions sources list           Lists configured marketplace sources.
  qwen extensions sources update <name>  Re-fetches a marketplace source and its plugin listing.

Interactive TUI: The Ink-based TUI renders to the alternate screen buffer, which makes capture-pane unreliable in this headless environment. The collaborator (wenshao) ran a full tmux real-user E2E pass at the same head covering all three tabs, install/uninstall/enable-disable across restarts, scope changes, MCP toggle, the CLI command group, zh rendering, ANSI injection resistance (byte-level PWNED 0 · OSC ]0; 0 · [2K 0), and mutation test on the url-source guard — and reported all passing with APPROVE.

中文说明

代码审查(在 head 158002c773 重审)

独立方案(阅读第 6 轮 diff 之前):对于市场管理器审查反馈的修复轮次,我预期:(1) 用与字符串 source 分支相同的守卫堵住 { source: 'url' } 本地路径绕过,(2) 把回滚失败显式化而非静默吞掉,(3) 在 marketplace name 成为 Discover 过滤值前做净化,(4) 添加移除守卫就会失败的测试。

与 diff 对比: 第 6 轮完全匹配所有四项预期。url-source guard(sourceRegistry.ts:182-196)使用同样的检查逻辑。回滚失败上报(DiscoverTab.tsxExtensionActionsView.tsx)两处都显式抛出带 i18n 的错误消息。Marketplace name 净化(ExtensionsManagerDialog.tsx:114-119)在 handleBrowseSource 中应用 stripUnsafeCharacters。Tab 清除行为修复匹配 (Tab to clear) 提示。

未发现关键阻断问题。

测试结果

  • 单元测试:325 项全部通过 ✅(core 170 + CLI UI 33 + CLI 命令 122)
  • 类型检查:所有包 clean ✅
  • CLI 命令sources 子命令组(add/list/update/remove)正确注册,help 输出完整 ✅,sources listextensions list 正确输出空状态
  • 交互式 TUI:Ink 备用屏幕渲染,无头环境 capture-pane 不可靠。协作者 wenshao 在同一 head 完成完整 tmux E2E 并通过 APPROVE。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Stepping back to look at the whole picture: this PR has been through 6 rounds of review from maintainers wenshao and DragonnZhang, covering security (ANSI injection, path traversal, fetch hardening), correctness (scope-change rollback, corrupt file handling), and architecture (component reuse, i18n parity). The latest round (6) closes a real install-source path-traversal bypass and surfaces a previously-silent double-failure, both with non-vacuous tests.

My independent proposal from Stage 2a matched the PR's approach — the PR actually went further with the rollback-failure surfacing pattern and the corrupt-file quarantine abstraction, both of which are the kind of defensive code I'd want to see in a feature that processes untrusted third-party marketplace data.

The scope is large but justified: 15 new files are all purpose-built for this feature, and the 38 modified files are either integration points (DialogManager, i18n, MCP components for reuse) or enhancements to existing extension logic (converter, manager, config). No drive-by refactors, no scope creep.

325 unit tests pass, typecheck is clean, CLI commands work as expected. The interactive TUI couldn't be verified via capture-pane in this headless environment (Ink alt-screen rendering), but the maintainer's comprehensive E2E pass — including byte-level ANSI injection verification and a mutation test proving the url-source guard is genuinely tested — covers that gap thoroughly.

If I had to maintain this in six months, I'd thank the author — the code is well-organized, the security boundaries are clearly marked with comments explaining why, and the test coverage is thorough.

Approving. ✅

中文说明

整体审视:本 PR 已经历维护者 wenshao 和 DragonnZhang 的 6 轮审查,涵盖安全(ANSI 注入、路径穿越、fetch 加固)、正确性(scope 变更回滚、损坏文件处理)和架构(组件复用、i18n 一致性)。最新第 6 轮堵上了一个真实的安装源路径穿越绕过,并把一个静默的双重失败显式化,二者都带非空泛测试。

我在 Stage 2a 的独立方案与 PR 实现一致——PR 实际上更进一步,增加了回滚失败上报模式和损坏文件隔离抽象,这些正是处理不受信任的第三方市场数据时应有的防御性代码。

体量虽大但合理:15 个新文件均为此功能专属,38 个修改文件要么是集成点(DialogManager、i18n、MCP 组件复用),要么是现有扩展逻辑的增强(转换器、管理器、配置)。无顺手重构,无范围蔓延。

325 项单元测试通过,类型检查 clean,CLI 命令工作正常。交互式 TUI 因 Ink 备用屏幕渲染无法通过 capture-pane 验证,但维护者的全面 E2E 测试——包括字节级 ANSI 注入验证和证明 url-source guard 非空泛的 mutation test——覆盖了此缺口。

六个月后维护此代码,我会感谢作者——代码组织良好,安全边界有清晰的 why 注释,测试覆盖充分。

批准 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot 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.

LGTM, looks ready to ship. ✅

…on, rollback-failure surfacing, url-source guard, security tests

Code:
- ANSI injection via the Discover marketplace filter: the marketplace name
  (untrusted, from a remote marketplace.json) flowed through onBrowse to the
  Discover hint render unsanitized. Scrub it in handleBrowseSource — this also
  fixes the filter comparison (it is matched against the already-sanitized
  DiscoveredPlugin.marketplaceName).
- '(Tab to clear)' hint was misleading: Tab cycled tabs rather than clearing
  the marketplace filter in place. On Discover with an active filter, Tab now
  clears the filter in place, matching the hint.
- Scope-change rollback failures were silently swallowed by a bare catch in
  both the Discover batch install and ExtensionActionsView, unlike the CLI
  install.ts which surfaces them. Both now report the rollback failure so the
  user knows the extension may be disabled at all scopes (new i18n keys for
  en/zh/zh-TW).
- resolveInstallSource: the structured { source: 'url' } branch bypassed the
  local-path guard applied to string sources, letting a remote http
  marketplace redirect the installer at a local filesystem path. Apply the
  same guard.

Tests:
- marketplace fetchUrl: wall-clock deadline (stalled server) and body-size cap
  (oversized stream) now covered.
- sourceRegistry: remote-marketplace local-path rejection covered for both the
  string and { source: 'url' } source forms.
- claude-converter git-subdir: clone+sha-pin happy path plus path-escape,
  absolute-path, missing-subdir, and symlink-escape rejections covered.

Note: the bot's 'missing scope rollback' threads target a pre-631e271fb
snapshot — that rollback already landed in round 5.
@BZ-D

BZ-D commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for round 6 @wenshao and the /review bot 🙏. Pushed 158002c77; replied inline on each thread.

✅ Fixed — security / correctness

  • ANSI injection via the Discover marketplace filter — the untrusted marketplace name flowed through onBrowse to the Discover hint render unsanitized. Sanitized in handleBrowseSource; this also fixes the filter comparison (it's matched against the already-sanitized DiscoveredPlugin.marketplaceName).
  • { source: 'url' } bypassed the local-path guard — the structured url-source form skipped the absolute/./~ rejection the string form applies, letting a remote http marketplace redirect the installer at a local path. Same guard now applied.
  • Misleading (Tab to clear) hint — Tab cycled tabs instead of clearing the filter. On Discover with an active filter, Tab now clears it in place (done in the parent, since useKeypress has no propagation control). Hint is now accurate.
  • Rollback-failure swallowing — when a scope-change rollback (enableExtension(User)) itself failed, both the Discover batch install and ExtensionActionsView swallowed it with a bare catch, unlike CLI install.ts. Both now surface it so the user knows the extension may be disabled at all scopes and to re-enable it (new i18n keys for en/zh/zh-TW).

✅ Added — security tests

  • fetchUrl guards — wall-clock deadline (stalled server, fake timers) + body-size cap (11 MB stream → abort).
  • Remote local-path rejection — both string and { source: 'url' } forms.
  • git-subdir branch — clone + sha-pin happy path, plus path-escape / absolute-path / missing-subdir / symlink-escape rejections.

↩️ Already addressed (bot reviewed a pre-round-5 snapshot)

  • The two "missing scope-change rollback" threads (DiscoverTab:239, ExtensionActionsView:228) were fixed in 631e271fb (round 5) — the rollback is already present at HEAD. Round 6 went further and now also surfaces a rollback-failure.

Validation: core extension suites 95 ✅ (sourceRegistry 20 · marketplace 25 · claude-converter 40 · extensionPreferences 11 — includes the new tests); cli ExtensionsManagerDialog/install/sources 47 ✅ (NO_COLOR); check-i18n, tsc, eslint, prettier all clean.

Re-requesting review 🙏.

中文

第 6 轮已在 158002c77 处理,逐条 inline 回复:

  • 安全/正确性:Discover marketplace 过滤名 ANSI 注入(在 handleBrowseSource 清洗,顺带修复过滤比较)、{source:'url'} 绕过本地路径守卫(补同样守卫)、(Tab to clear) 提示与行为不符(改为在 parent 就地清除 filter)、rollback 自身失败被裸 catch 吞掉(Discover 批量安装与 ExtensionActionsView 现都向用户提示,新增 en/zh/zh-TW 文案)。
  • 安全测试:fetchUrl 的 wall-clock 超时 + body 上限;远程源本地路径拒绝(string 与 {source:'url'} 两种形式);git-subdir 分支(克隆+sha 固定的happy path,以及路径逃逸/绝对路径/子目录缺失/符号链接逃逸的拒绝)。
  • 已处理(bot 评的是 round-5 之前的快照):两个"缺少 scope rollback"线程在 631e271fb(第 5 轮)已修,HEAD 上 rollback 已存在;第 6 轮进一步补了 rollback 自身失败的提示。

验证:core 扩展套件 95 ✅、cli 47 ✅(NO_COLOR)、check-i18n / tsc / eslint / prettier 全绿。

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@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.

Round 6 review — build green, eslint clean, 309 core + 155 CLI extension tests pass. No high-confidence critical issues remain after 5 prior rounds of hardening. One new suggestion on an unhandled source variant, plus low-confidence terminal-only findings (code duplication, test coverage gaps, debuggability) listed in the Needs Human Review section of the terminal output.

}
return plugin.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] resolveInstallSource does not handle the git-subdir source variant defined in ClaudePluginSource (claude-converter.ts:98). When a remote (HTTP) marketplace plugin declares { source: 'git-subdir', url, path, ref?, sha? }, the function falls through to return plugin.name — a bare name that parseInstallSource cannot resolve to an installable artifact.

The sibling resolvePluginSource in claude-converter.ts:1088 correctly handles git-subdir during the install phase, but the Discover tab's install flow depends on this function to produce the installSource string. Without a matching branch, git-subdir plugins from HTTP marketplaces fail to install from the Discover tab.

Suggested change
if (src && src.source === 'url') {
return src.url;
}
if (src && src.source === 'git-subdir') {
return src.url;
}
return plugin.name;

— qwen3.7-max 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.

Thanks — I looked into this again rather than re-deferring, and the return src.url one-liner would introduce a silent wrong-install, so I'd like to push back and propose a proper follow-up instead.

Why return src.url is unsafe: a git-subdir source is { url, path, ref?, sha? } — the plugin lives in subdirectory path, optionally pinned to ref/sha. But ExtensionInstallMetadata (config.ts:540) only has source / type / ref — there is no subdirectory field, and parseInstallSource has no subdir syntax. So return src.url drops both path and the pin and clones the repo root: it then installs the whole repo as a standalone extension, or (if the repo ships its own marketplace.json) prompts the user to pick an unrelated plugin. Either way it installs something other than the subdir plugin the entry describes — strictly worse than today's explicit failure.

Scope of the gap: string- and github-form sources from an HTTP marketplace already install git-subdir correctly, because their installSource is a repo:plugin form that routes back through convertClaudePluginPackageresolvePluginSource, which fully handles git-subdir (now covered by the new tests in 158002c77). The unsupported case is narrowly a direct { source: 'git-subdir' } entry in an http-type (direct-JSON) marketplace.

Proper fix: thread the subdir + ref through the install pipeline (add a path/subdir field to ExtensionInstallMetadata and teach the git clone to check out the subdir at the pinned ref) — a self-contained capability with its own tests that I'd prefer to land as a follow-up rather than fold into this already-large PR. If you'd rather have it in-scope here, say the word and I'll add it (with tests) in the next push. (Mirrors my reply on the original thread for this same item.)

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

✅ Re-verification at new head 158002c773 (maintainer) — recommendation stands: merge

Supersedes my round-4/5 verification (head 631e271fba). One new commit since — review round 6 (158002c773, +302/-8, 10 files). Headline: it closes a real path-traversal bypass (structured url install-source) and surfaces a previously-silent data-integrity failure, both with new tests. Re-verified on macOS.

Re-check (delta 631e271fba..158002c773) Result
npm ci + npm run build (macOS) ✅ exit 0
Round-6 core suites (+235 lines of new security tests) 170 passed
Mutation test on the url-source guard ✅ removing it fails the new test → non-vacuous
Real-TUI smoke on the round-6 binary ✅ 3 tabs OK, ANSI sanitization intact, no regression
CI Lint ✅ · 3× Test + CodeQL still pending

1. What round 6 changed

  • url-source path-traversal guard (the security fix). resolveInstallSource already refused local-filesystem paths from a remote http marketplace in the bare-string source form (/etc/passwd, ../x, ~/x). Round 6 extends the identical guard to the structured { source: 'url', url: … } form, which previously bypassed it — a hostile marketplace could set { source: 'url', url: '/etc/shadow' } and redirect the installer at a local path. Now it falls back to the plugin name; genuine https://… URLs are preserved.
  • Rollback-failure surfacing (data-integrity UX). The scope-change flow disables at User scope then enables at Workspace scope; if the enable fails it rolls the User enable back. If the rollback itself also failed, both ExtensionActionsView and DiscoverTab previously swallowed it (catch {}) — leaving the extension disabled at every scope with the user told it succeeded. Both now surface it explicitly ("…may be disabled at all scopes; re-enable it from the Installed tab").
  • Marketplace-name filter sanitization (defense-in-depth: the Browse→Discover filter value is run through stripUnsafeCharacters), plus i18n strings and +235 lines of new security tests (claude-converter +133, marketplace +57, sourceRegistry +45).

2. Tests (macOS)

core (6): sourceRegistry 19 · marketplace 25 · claude-converter 40 · extensionPreferences 11
          extensionManager 50 · textUtils 25                              -> 170 passed

3. Mutation test — the url-source guard is genuinely tested ⭐

The new discoverPlugins > rejects local-path sources from a remote (http) marketplace test asserts both source forms (bare-string and structured url) for absolute / relative / ~ paths fall back to the plugin name, while a real https://… URL is preserved. I reverted just the round-6 guard ({ source:'url' } branch back to return src.url) and re-ran:

× discoverPlugins > rejects local-path sources from a remote (http) marketplace
   -> expected '/etc/shadow' to be 'urlabs'        (installer would be pointed at /etc/shadow)
Tests  1 failed | 18 passed (19)

Exactly that test fails, and the failure value /etc/shadow is the vulnerability the guard closes. Restored -> 19/19 green.

4. Real-TUI smoke (no regression from the DiscoverTab / ExtensionActionsView edits)

Round-6 binary, isolated $HOME, hostile-marketplace fixture. All three tabs still render and navigate; the hostile plugin still sanitizes (evil<ESC>[31m-x<ESC>[2K -> evil-x, desc -> safe ok); byte-level PWNED 0 · OSC ]0; 0 · [2K 0; pane title stays "Qwen - project". The round-6 changes are guard / error-path logic, so the decisive proof is the mutation above; the smoke confirms the UI edits didn't regress rendering.

Verdict

Round 6 is a clean security + data-integrity hardening round: a real install-source path-traversal bypass closed (with a non-vacuous test), a silent double-failure made visible, all green locally. Recommendation stands: merge once the pending CI matrix is green. (As before, a red macOS Test job would most likely be the unrelated cronScheduler durable-ownership timing flake, not this PR.)

🇨🇳 中文版(点击展开)

✅ 在新 head 158002c773 上的复验(维护者)—— 结论不变:建议合并

本评论取代我第 4/5 轮的验证(head 631e271fba)。之后新增一个 commit —— review 第 6 轮(158002c773,+302/-8,10 文件)。要点:它堵上了一个真实的路径穿越绕过(结构化 url 安装源),并把一个此前静默的数据完整性失败显式化,二者都带新测试。已在 macOS 复验。

复检项(增量 631e271fba..158002c773) 结果
npm ci + npm run build(macOS) ✅ exit 0
第 6 轮 core 套件(+235 行新安全测试) 170 通过
url-source guard 的 mutation test ✅ 移除后新测试失败 → 非空泛
round-6 二进制实机冒烟 ✅ 三 tab 正常、ANSI 净化完好、无回归
CI Lint ✅ · 三平台 Test + CodeQL 仍 pending

1. 第 6 轮改了什么

  • url-source 路径穿越 guard(安全修复)。 resolveInstallSource 此前已对远程 http marketplace 的裸字符串 source 形式拒绝本地路径(/etc/passwd../x~/x)。第 6 轮把同样的 guard 扩展到结构化 { source:'url', url:… } 形式 —— 它此前能绕过:恶意 marketplace 可设 { source:'url', url:'/etc/shadow' } 把安装器重定向到本地路径。现在会 fallback 到 plugin name;真实 https://… URL 保留。
  • rollback 失败上报(数据完整性 UX)。 scope 变更流程先 disable User 再 enable Workspace;若 enable 失败则回滚 User enable。若回滚本身也失败,ExtensionActionsViewDiscoverTab 此前都把它静默吞掉(catch {})—— 扩展在每个 scope 都被禁用,却告诉用户成功了。现在两处都显式上报("可能在所有 scope 都被禁用;从 Installed tab 重新启用")。
  • marketplace-name 过滤净化(纵深防御:Browse→Discover 的过滤值过一遍 stripUnsafeCharacters),外加 i18n 字符串和 +235 行新安全测试(claude-converter +133、marketplace +57、sourceRegistry +45)。

2. 测试(macOS)

core (6): sourceRegistry 19 · marketplace 25 · claude-converter 40 · extensionPreferences 11
          extensionManager 50 · textUtils 25                              -> 170 通过

3. Mutation test —— url-source guard 确有测试守护 ⭐

新测试 discoverPlugins > rejects local-path sources from a remote (http) marketplace 断言两种 source 形式(裸字符串与结构化 url)的绝对/相对/~ 路径都 fallback 到 plugin name,而真实 https://… URL 保留。我只把第 6 轮的 guard 还原({ source:'url' } 分支退回 return src.url)后重跑:

× discoverPlugins > rejects local-path sources from a remote (http) marketplace
   -> expected '/etc/shadow' to be 'urlabs'        (安装器会被指向 /etc/shadow)
Tests  1 failed | 18 passed (19)

恰好该测试失败,且失败值 /etc/shadow 正是 guard 所堵的漏洞。还原后 -> 19/19 绿。

4. 实机冒烟(DiscoverTab / ExtensionActionsView 改动无回归)

round-6 二进制、隔离 $HOME、恶意 marketplace fixture。三个 tab 仍正确渲染和导航;恶意插件仍被净化(evil<ESC>[31m-x<ESC>[2K -> evil-x,描述 -> safe ok);字节级 PWNED 0 · OSC ]0; 0 · [2K 0;pane 标题仍为 "Qwen - project"。第 6 轮是守卫/错误路径逻辑,决定性证明是上面的 mutation;冒烟确认 UI 改动没有破坏渲染。

结论

第 6 轮是干净的安全 + 数据完整性加固:堵上了一个真实的安装源路径穿越绕过(带非空泛测试),把一个静默的双重失败变可见,本地全绿。结论不变:建议合并 —— 待 pending 的 CI 矩阵转绿即可。(同前,若 macOS Test 变红,大概率仍是与本 PR 无关的 cronScheduler durable-ownership 计时 flaky。)

Maintainer re-verification · head 158002c773 (prev 631e271fba) · macOS 26.5 / Node 22.22.2.

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot 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.

LGTM, looks ready to ship. ✅

@wenshao
wenshao merged commit 61dcf86 into main Jun 20, 2026
150 of 155 checks passed

@qwen-code-ci-bot qwen-code-ci-bot 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.

Code Review Summary

Verdict: Request changes | 9 review agents | 18/19 findings verified | 1 reverse-audit round

This review identified 5 new findings not yet covered in the existing PR discussion threads. Many other issues found by our agents were already discussed and/or fixed in earlier review rounds — thanks to the reviewers for the thorough prior passes.

Critical (3)

  1. marketplace.ts:262loadMarketplaceConfigFromSource("owner/repo") passes through stat() at Priority 1 before the owner/repo GitHub shorthand is checked at Priority 4. If a local directory matching owner/repo exists in cwd, the function silently returns the local marketplace config instead of fetching from GitHub. Fix: add a shorthand-detection heuristic before stat() to skip it for likely owner/repo patterns.

  2. sourceRegistry.ts:345discoverPlugins dedup key ${plugin.marketplaceName}/${plugin.name} uses display names, not source URLs. Two distinct plugins from different sources that share the same marketplace + plugin name collide, and one is silently dropped. Fix: include the source URL or registry identifier in the dedup key.

  3. config.ts:3254isMcpServerDisabled() only checks extensionPreferences.disabledMcpServers, missing runtime MCP servers. An extension-bundled server name that coincidentally matches a disabled entry reports disabled: true even when loaded via the runtime path.

Suggestion (2)

  1. extensionPreferences.ts:105favorites array validated with Array.isArray only, no element-type filter. Sibling fields (scopes, disabledMcpServers) validate both container and element types. Fix: Array.isArray(parsed.favorites) && parsed.favorites.every((v: unknown) => typeof v === 'string').

  2. sourceRegistry.test.ts — No vi.mock('fs') setup. Tests create real temp files via os.tmpdir(), which is fragile (race conditions, leftover files). Sibling extensionPreferences.test.ts uses vi.mock('fs'). Recommend matching that pattern.


Findings deduplicated against 160+ existing PR comments. Build and all 378 unit tests pass.

— qwen3.7-max via Qwen Code /review

const trimmed = source.trim();

// Priority 1: local path (directory with .claude-plugin/marketplace.json,
// or a direct marketplace.json file).

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] Priority 1 local-path check shadows the owner/repo GitHub shorthand (Priority 4).

loadMarketplaceConfigFromSource("owner/repo") passes "owner/repo" to stat() at Priority 1. On Linux, stat("owner/repo") succeeds if a local directory ./owner/repo/ exists — and the function returns the local marketplace config instead of resolving the GitHub shorthand at Priority 4.

This is a subtle correctness issue: a user running the CLI from a directory that happens to contain a folder matching owner/repo silently gets the wrong marketplace data.

Fix: before the stat() call, add a heuristic that skips Priority 1 for sources that look like a GitHub shorthand:

Suggested change
// or a direct marketplace.json file).
// Priority 1: local path — skip for likely GitHub shorthands (owner/repo)
const looksLikeShorthand = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(trimmed);
if (!looksLikeShorthand) {
try {
const stats = await stat(trimmed);
if (stats.isDirectory()) {
return await readLocalMarketplaceConfig(trimmed);
}
if (stats.isFile()) {
try {
const content = await fs.promises.readFile(trimmed, 'utf-8');
return JSON.parse(content) as ClaudeMarketplaceConfig;
} catch {
return null;
}
}
} catch {
// Not a local path — fall through to next priority
}
}

Alternatively, reorder priorities so the owner/repo check runs before the local-path stat.

— qwen3.7-max via Qwen Code /review

const seen = new Set<string>();
const deduped: DiscoveredPlugin[] = [];
for (const plugin of results.flat()) {
const key = `${plugin.marketplaceName}/${plugin.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.

[Critical] discoverPlugins dedup key uses display names, causing cross-source collisions.

The dedup key ${plugin.marketplaceName}/${plugin.name} uses display names, not source URLs. Two distinct plugins from different sources that share the same marketplace + plugin name collide, and one is silently dropped.

Example: Source A (GitHub) and Source B (HTTP marketplace) both declare a plugin named "my-ext" under marketplace "MyMarket". The second is dropped even though they may be entirely different plugins.

Fix: include the source URL or registry identifier in the dedup key:

Suggested change
const key = `${plugin.marketplaceName}/${plugin.name}`;
const key = `${plugin.source ?? 'unknown'}:${plugin.marketplaceName}/${plugin.name}`;

Or, if DiscoveredPlugin carries a sourceUrl / registryUrl field, use that instead.

— qwen3.7-max via Qwen Code /review

}
}
const prefs: ExtensionPreferences = {
favorites: Array.isArray(parsed.favorites) ? parsed.favorites : [],

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] favorites array is not validated for element types, unlike scopes and disabledMcpServers.

Array.isArray(parsed.favorites) accepts any array, including [1, null, true, "real-ext"]. The sibling fields use proper element-type guards:

// scopes — validates each entry
scopes: isRecord(parsed.scopes) ? Object.fromEntries(...) : {}
// disabledMcpServers — validates each entry
disabledMcpServers: isRecord(parsed.disabledMcpServers) ? Object.fromEntries(...) : {}

Fix:

Suggested change
favorites: Array.isArray(parsed.favorites) ? parsed.favorites : [],
favorites: Array.isArray(parsed.favorites) && parsed.favorites.every((v: unknown) => typeof v === 'string') ? parsed.favorites : [],

While non-string elements would never match .includes(name) (since name is a string), the inconsistency can mask bugs at deserialization time.

— qwen3.7-max via Qwen Code /review

@@ -3252,7 +3252,25 @@ export class Config {
}

isMcpServerDisabled(serverName: string): boolean {

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] isMcpServerDisabled() doesn't check runtimeMcpServers, leading to false positives.

Runtime MCP servers (registered programmatically by extensions or plugins, not from config files) are not checked here. If an extension registers a runtime server named "foo", and an unrelated extension-bundled server named "foo" is disabled in preferences, isMcpServerDisabled('foo') returns true — incorrectly suppressing the runtime server.

The check should distinguish runtime from extension-bundled servers:

Suggested change
isMcpServerDisabled(serverName: string): boolean {
isMcpServerDisabled(serverName: string): boolean {
const prefs = this.getExtensionPreferences();
const isDisabled = prefs.disabledMcpServers[serverName] === true;
// Runtime MCP servers are not managed by extension preferences
if (this.isRuntimeMcpServer?.(serverName)) {
return false;
}
return isDisabled;
}

— qwen3.7-max via Qwen Code /review

@@ -0,0 +1,316 @@
/**

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] sourceRegistry.test.ts lacks vi.mock('fs') — tests hit real filesystem.

The test file imports SourceRegistryStore which uses fs.readFileSync/fs.writeFileSync internally. Unlike sibling extensionPreferences.test.ts, there is no vi.mock('fs') setup.

Tests create real temp files via os.tmpdir(), which works but is fragile:

  • Race conditions with concurrent test runs
  • Leftover temp files on crash
  • Platform-specific path behavior differences

Recommend adding vi.mock('fs') with memfs or vitest's built-in vi.fs() mock to match the sibling test pattern:

vi.mock('fs');
// or
vi.mock('node:fs');
vi.mock('node:fs/promises');

— qwen3.7-max via Qwen Code /review

@BZ-D
BZ-D deleted the feat/enhance-extensions-command branch June 29, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants