Commit graph

13 commits

Author SHA1 Message Date
jinye
a234860a4a
fix(core): Align MCP OAuth guidance and docs (#5589)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* docs: Align docs with current CLI behavior

Update stale documentation and user-facing MCP OAuth guidance to match the current dialog-based flows, SDK permission semantics, current links, and Qwen OAuth status.

Also replace Ink internal imports with public Ink APIs for the shared text input so the workspace builds against Ink 7.

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

* codex: address PR review feedback (#5589)

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

* codex: address PR review feedback (#5589)

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

* codex: address PR review feedback (#5589)

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

* codex: address PR review feedback (#5589)

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

* codex: address PR review feedback (#5589)

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

* codex: address PR review feedback (#5589)

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

* codex: address PR review feedback (#5589)

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

* codex: address PR review feedback (#5589)

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

* codex: address PR review feedback (#5589)

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

* codex: address PR review feedback (#5589)

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

* codex: fix BaseTextInput Ink import (#5589)

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

* codex: address PR review feedback (#5589)

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

* fix(core): surface MCP OAuth credential read failures

Fix SSE OAuth credential pre-check failures by reporting token storage read errors before connecting.

Update SDK coreTools docs and extension release link text from the follow-up review.

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

* fix(core): harden MCP OAuth error handling

Handle stderr warning failures as best-effort and keep SSE 401 OAuth guidance when credential re-read fails.

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

* fix(core): keep SSE OAuth pre-read best effort

Avoid blocking SSE MCP connections when the diagnostic credential pre-read fails, and cover BaseTextInput absolute-position edge cases.

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

* fix(core): handle SSE OAuth validation errors

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

* fix(core): surface MCP OAuth recovery guidance

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

* fix(core): cover MCP OAuth retry paths

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

* fix(core): address OAuth guidance review

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-24 07:09:53 +08:00
kkhomej33-netizen
75fc0a5c18
feat(extensions): support archive install sources (#4909)
* feat(extensions): support archive install sources

* fix(core): harden extension archive installs
2026-06-22 13:36:13 +08:00
Edenman
61dcf865de
feat(extensions): interactive multi-tab /extensions manager (Installed / Discover / Sources) (#4850)
* feat(extensions): multi-tab /extensions dialog (Discover/Installed/Marketplaces)

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.

* feat(extensions): align Discover plugin detail with Claude Code

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.

* feat(extensions): align Add Marketplace view with Claude Code

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.

* feat(extensions): fix Discover hang + add search/scrolling, align list 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.

* feat(extensions): align marketplace detail with CC + Browse-to-Discover

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.

* feat(extensions): show plugin type in Installed; guide single-extension 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.

* fix(extensions): resolve git@ SSH marketplace sources

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.

* feat(extensions): cap Discover list window at 6 items

* feat(extensions): unify 'Extension' wording, reorder tabs, expand Marketplaces 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.

* feat(extensions): update Marketplaces tab footer hint

* feat(extensions): full extension actions in both tabs + context-aware 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.

* feat(extensions): rename Marketplaces tab to Sources, hide Favorites + 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.

* feat(extensions): group Sources action rows + show current scope in selector

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

* fix(extensions): move uninstall note to confirm step; complete zh/zh-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.

* feat(extensions): label Installed scope groups as 用户级/项目级/本地级

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.

* feat(extensions): reuse /mcp server detail for installed MCP servers

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.

* fix(extensions): populate MCP promptCount from prompt registry

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.

* refactor(extensions): rename the source-management layer from marketplace 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.

* fix(extensions): keep marketplaces.json filename so saved sources survive 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.

* fix(extensions): stay on the Discover detail when an install fails

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.

* feat(extensions): support 'git-subdir' plugin source in Claude marketplaces

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

* feat(extensions): drop the unused 'local' install scope

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.

* feat(extensions): add Ctrl+R shortcut to refresh Discover tab

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.

* fix(extensions): keep j/k typeable in Discover search

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.

* feat(extensions): install standalone Claude Code plugins from a git URL

A repo whose root holds .claude-plugin/plugin.json (no marketplace.json) is
a standalone Claude plugin. Previously installing one by git URL failed with
"Configuration file not found" because the converter only handled gemini
extensions and marketplace-based Claude plugins.

Add convertClaudePluginStandalone: read plugin.json, fold MCP servers from a
root .mcp.json into the config, collect skills/commands/agents, and write
qwen-extension.json. The marketplace path is refactored to share the build
step. MCP entries are normalized from Claude's transport shape (type:'http' +
url) to Qwen's (httpUrl), and the cloned .git is dropped from the result.

Note: cloneFromGit does not init git submodules, so submodule-provided skills are not installed yet.

* i18n(zh): relabel user-scope install as 全局安装

The Discover detail's user-scope install option now reads 全局安装(用户作用域) instead of 仅为你安装(用户作用域), which better conveys that user scope installs the extension globally.

* fix(extensions): keep the manager mounted during install consent prompts

Consent, setting-input and plugin-choice requests raised by an install
used to replace the ExtensionsManagerDialog in DialogManager, unmounting
it mid-install: the dialog remounted on the default Installed tab with
pre-install data, and the completion reload signal hit the dead
instance. Render those prompts inside the dialog instead (tab content
hidden but mounted), and gate the previously always-active key handlers
(MCP detail steps, uninstall confirm) so hidden views can't double-handle
keys while a prompt is shown.

* feat(extensions): show loading feedback for scope change and toggles

Changing an extension's scope now swaps the selector for a
"Changing scope..." line (Esc ignored while in flight), and the
Installed tab's Space toggle reports "Enabling/Disabling ..." in the
status line right away — MCP enable rediscovers tools and can take a
while. Overlapping toggle/favorite presses are ignored until the
in-flight mutation finishes.

* feat(extensions): add --scope to install and a sources CLI command group

Bring the qwen extensions CLI up to par with /extensions manage:
install --scope user|project (workspace accepted as an alias) records
the scope preference and, for project, re-scopes enablement to the
current workspace only — mirroring the Discover tab's install flow.
New sources add/list/update/remove subcommands manage the Claude
marketplace sources that power the Discover tab.

* feat(extensions): trim the Sources tab and add marketplace detail retry/refresh

Drop the redundant installed-extensions list from the Sources tab (those
live on the Installed tab); the marketplace detail still summarizes which
of its plugins are installed, now capped with a "… and N more" line so a
long list stays short. Add an R key in the marketplace detail that
re-fetches the source — surfaced in the footer as a retry on load
failure and a refresh once loaded. Reword the install row to "Install a
new extension".

* feat(extensions): nest extension-bundled MCP servers under their extension

The Installed tab now lists each extension's bundled MCP servers as
indented child rows beneath it, with Enter opening the shared MCP detail
view (tools, OAuth authenticate/clear) just like the /mcp panel.

- Skip child rows shadowed by a same-named user/project server and ones
  blocked by the MCP allow-list, matching the runtime merge semantics.
- Space only blocks the disable direction for bundled servers; an
  individually-excluded server under an active extension can be
  re-enabled. Blocked Space/favorite and Enter on a disabled extension's
  server give info feedback instead of failing silently.
- Hide the always-failing Disable action for active extension-provided
  servers in ServerDetailStep (also fixes the /mcp panel).
- Window the Installed list to the terminal height with scroll hints,
  clamped offset and group-header anchoring.
- Fall back to the list if the open detail's item disappears on reload
  so the tab can't get stuck locked.
- Hermetic tests: stub loadSettings, stabilize two flaky assertions.

* feat(extensions): per-server disable for extension MCPs and live status

Extension-bundled MCP servers can now be disabled individually, aligned
with Claude Code. The record lives in extension-preferences.json keyed
by extension name (not the global mcp.excluded list), so it never
affects same-named servers from other sources, survives restarts via
config.isMcpServerDisabled, and is cleaned up on uninstall.

- All three surfaces support the toggle: Installed tab Space, the
  extensions MCP detail view, and the /mcp panel; ServerDetailStep
  offers Disable for extension servers again.
- Installed tab MCP rows now show the live connection state (connected
  / needs authentication / connecting / disconnected) instead of a bare
  enabled flag; selected rows highlight the badge and status text too.
- "Needs authentication" (401 marker or declared OAuth with no stored
  token) renders consistently in the /mcp list and both detail views;
  a successful connect clears the sticky 401 marker in core.
- All three surfaces subscribe to MCP status changes for live updates,
  with statuses re-stamped synchronously before setState to close the
  load/listener race.
- Perf: extension preferences are cached by mtime, and the disabled
  predicate finds the owning extension without rebuilding the merged
  server map.

* docs(extensions): document interactive manager and backfill missing i18n

Add a 'The interactive extension manager' section covering the
Discover/Installed/Sources tabs, per-server MCP enable/disable, and
keybindings.

Backfill en/zh/zh-TW entries for six previously-untranslated strings
referenced via t() (npm install flags, the install command description,
'Description', and 'Delete Session') so they render localized instead of
falling back to English.

* fix(extensions): harden untrusted-marketplace handling from PR review

Address review findings on the new marketplace/plugin attack surface:

Security
- Strip ANSI/control sequences from marketplace-sourced display strings
  (Discover + Sources) so a hostile source can't manipulate the terminal
  before install consent.
- Confine git-subdir source.path to the cloned repo (reject absolute/.. /empty)
  to stop path traversal; prefer the immutable SHA pin over a named ref.
- Refuse absolute/relative local-path plugin sources from remote marketplaces.
- Reject absolute/escaping mcpServers and hooks file paths in plugin.json so
  the converter can't read arbitrary out-of-tree files.
- Only follow http(s) plugin homepages (block file:// etc. via open()).
- Cap marketplace HTTP response bodies and add a wall-clock fetch deadline
  (req.setTimeout is socket-idle only) to prevent OOM / indefinite hangs.

Correctness / robustness
- A post-install scope/enablement failure no longer marks the install failed.
- .mcp.json without an mcpServers object is skipped instead of misparsed.
- Guard Ctrl+R refresh against concurrent in-flight discovery.
- Log (not swallow) OAuth token-store lookup failures.
- InstalledTab: single-pass tool-count map (drop N+1 getAllTools) and O(1)
  row index lookup (drop per-row items.indexOf).

Tests
- Make the .git-stripping assertion meaningful and cover the absolute-path
  mcpServers guard and the .mcp.json fallback.

* fix(extensions): consent layout, per-extension update check, uninstall progress

- ConsentPrompt: render the markdown prompt in a column. The inner Box
  defaulted to flexDirection=row, so a multi-paragraph consent (e.g. an
  extension bundling several MCP servers) tiled its blocks into narrow
  vertical columns. It now stacks vertically.
- "Mark for Update" now checks only the selected extension via
  checkForExtensionUpdate (previously ran the full checkForAllExtensionUpdates
  with a discarded result), stores the result, and reports "update available"
  vs "already up to date" so the "Update Now" action shows up immediately.
- Uninstall: show an "Uninstalling ..." line while removal runs so the confirm
  prompt no longer looks frozen after Enter.
- i18n (en/zh/zh-TW): add the new strings; drop the now-unused
  "Checked ... for updates." key.

* feat(extensions): clearer update-check feedback for "Mark for Update"

Distinguish the four check outcomes instead of collapsing everything to
"up to date": update-available, up-to-date, not-updatable, and error.
For not-updatable Claude marketplace plugins, spell out the reason and
workaround (they are install-time conversions with no git remote, so they
update by reinstalling). Also show a "Checking ... for updates..." line
first, since git/github-release/npm checks hit the network.

* fix(extensions): confine resource/source paths, sanitize homepage (review round 2)

- claude-converter: route commands/skills/agents resource paths (collectResources)
  and the relative string plugin-source through path-confinement — reject
  absolute / ..-escaping values — matching the existing mcpServers/hooks guard.
- sourceRegistry: sanitize plugin.homepage like the other untrusted marketplace
  display fields (it otherwise reaches status toasts unsanitized).
- InstalledTab: construct MCPOAuthTokenStorage once instead of once per server.

* fix(extensions): confine symlink targets when converting untrusted plugins

A plugin source from an untrusted marketplace/git repo can embed a
symlink whose name stays inside the package but whose target points at a
host file (e.g. skills/leak.txt -> ~/.ssh/id_rsa). The path-confinement
checks were purely lexical (path.resolve), while the downstream
copy/read calls follow symlinks, so the target's content could be
shipped into the installed extension.

- copyDirectory: pin the package real-path root and skip symlinks whose
  resolved target escapes it (this is the bulk-copy vector at
  buildQwenExtensionFromPlugin).
- resolvePluginRelativeFile: re-verify the real path with realpathSync
  after the lexical check (covers mcpServers/hooks/single-file resources).
- collectResources: skip symlinked files in a collected folder whose
  target escapes the resource dir.
- resolvePluginSource: reject a string source that resolves outside the
  marketplace dir through a symlink.

Adds regression tests for the bulk-copy and collected-folder paths.

* fix(extensions): confine manifest reads, share path-containment helpers

Follow-up to the symlink confinement work (review round 3):

- Guard the three manifest reads that bypassed the new checks: a hostile
  clone could make marketplace.json / plugin.json / .mcp.json themselves
  symlinks to JSON-shaped host files (e.g. ~/.docker/config.json), whose
  content was parsed into the merged config. Each read now verifies the
  real path stays inside the package before reading; .mcp.json is treated
  as absent, plugin.json/marketplace.json throw.
- Hoist the containment logic into gemini-converter as exported
  isPathWithin (lexical) + realPathWithin (symlink-resolved) so the rule
  lives in one place instead of being duplicated across both converters.
- Document copyDirectory's confineRoot parameter in its JSDoc.

Adds targeted tests for the resolvePluginRelativeFile and
resolvePluginSource symlink-rejection branches and the plugin.json
manifest guard.

* fix(extensions): cap manager dialog width to the main content area

On a wide terminal the /extensions manager clipped its right-aligned
status column to a sliver (e.g. "扩…"). Root cause: the dialog sized
itself to boxWidth = columns - 4 with no cap, while the app's main
content area is capped at 100 columns (AppContainer's mainAreaWidth).
Past ~104 columns the dialog grew wider than its container and the right
edge — including the status column flexGrow pushes to the far right — was
clipped off-screen. Narrow terminals were unaffected because columns - 4
stayed within the cap.

Cap boxWidth at Math.min(columns - 4, 100) to match the main content
area, mirroring the existing DiffDialog idiom.

Adds a wide-terminal regression test that renders through a 200-column
stdout and asserts no line exceeds the content area (the uncapped dialog
produced ~196-col lines).

* fix(diff): cap /diff dialog width to the main content area

Same wide-terminal clipping as the extensions manager: DiffDialog sized
itself to Math.min(columns - 4, 110), but the app's main content area is
capped at 100 cols (AppContainer's mainAreaWidth). On a wide terminal the
dialog grew to 110 columns inside the 100-column container and its right
border/edge was clipped off-screen. Narrow terminals were unaffected
because columns - 4 stayed within the cap.

Cap dialogWidth at Math.min(columns - 4, 100) to match the container.

Adds a wide-terminal regression test (200-column stdout) asserting no
rendered line exceeds the content area.

* fix(extensions): address review round 3 — symlink/ANSI confinement, crash & data-loss hardening

Security:
- claude-converter: confine git-subdir subdir against symlink escape;
  fail strict mode on a symlinked plugin.json; sanitize untrusted
  source/path in conversion error messages
- sourceRegistry: sanitize version/category/lastUpdated and component
  names before TUI render
- gemini-converter: guard gemini-extension.json reads with realPathWithin
- SourcesTab: sanitize persisted marketplace name in list + remove-confirm

Robustness:
- SourcesTab: wrap sync removeSource in try/catch (was crashing the TUI);
  only mark a marketplace updated when the refresh actually loaded
- InstalledTab: move bundled-MCP enable write inside try/catch
- DiscoverTab: return to the list (not an arbitrary plugin's detail) after
  a failed batch install
- install.ts: roll back the User-scope disable when the Workspace enable fails
- claude-converter: reject a null/non-object plugin.json with a clear error;
  warn instead of silently skipping a symlinked .mcp.json
- extensionPreferences/sourceRegistry: quarantine a corrupt state file to
  ${path}.corrupted so the next write can't clobber recoverable data

Tests:
- cover marketplace.json/plugin.json/.mcp.json symlink guards, strict-mode
  rejection, and null plugin.json
- delete the process.stdout.columns override on non-TTY in dialog width tests

* fix(extensions): guard toggleFavorite write against unhandled rejection

* test(extensions): normalize realpathSync mock so gemini guard passes on Windows

* fix(extensions): address review round 4 — shared sanitizer, narrowed quarantine, visible warnings, tests

- consolidate the three drifted ANSI/control-char strippers into a single
  shared stripAnsiAndControl in core textUtils (fixes the C1-range gap in
  workflow-orchestrator's copy); sourceRegistry/claude-converter now delegate
- claude-converter: also sanitize the git-subdir ref/sha in the not-found error
- corruptFile: surface the quarantine on stderr (debug log is gated off, so a
  silent move would still look like data loss)
- extensionPreferences/sourceRegistry: only quarantine on a JSON parse failure,
  not on transient read errors (EACCES/EMFILE/EISDIR) that leave a valid file
- install: surface a failed scope-change rollback instead of swallowing it

Tests:
- gemini-converter: negative-path coverage for the realPathWithin guards
- new corruptFile.test (rename-aside + best-effort failure)
- stripAnsiAndControl unit tests (ANSI/OSC/C0/C1)
- install rollback + rollback-also-fails cases

* test(extensions): assert discoverPlugins strips ANSI/control chars from display fields

Locks in the untrusted-metadata sanitization that was only covered
empirically before. Feeds a hostile plugin (cursor moves, line clears,
OSC title-injection, BEL) through discoverPlugins and asserts every
rendered field — name/version/description/author/homepage/category/
lastUpdated/component names plus the marketplace name — comes out clean.

Addresses the maintainer verification note on PR #4850.

* fix(extensions): address review round 5 — scope-change rollback, version 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.

* fix(extensions): address review round 6 — marketplace-name sanitization, 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.

---------

Co-authored-by: 克竟 <dingbingzhi.dbz@alibaba-inc.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-20 13:18:19 +08:00
ChiGao
cadda23782
chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860)
* chore(deps): upgrade ink 6.2.3 -> 7.0.2 + bump Node engine to 22

ink 7 requires Node >=22 and react-reconciler 0.33 with React >=19.2,
so this PR also bumps:

- Node engines (root + cli + core) 20 -> 22
- React/react-dom 19.1 -> 19.2.4 (pinned exact via overrides to keep
  the transitive React graph deduped to a single instance)
- @types/node pinned to 20.19.1 via overrides to avoid an unrelated
  Dirent NonSharedBuffer regression in sessionService tests
- @vitest/eslint-plugin pinned to 1.3.4 to avoid an unrelated lint
  regression introduced by the 1.6.x rule additions
- react-devtools-core 4.28 -> 6.1 (ink 7 peerOptional requires >=6.1.2)
- ink hoisted to root devDeps so workspace-private peer-dep contention
  doesn't push ink-link/spinner/gradient into nested workspace
  installs (which would skip transitive resolution for terminal-link)

Workflow + image + installer alignment:

- .nvmrc 20 -> 22
- Dockerfile node:20-slim -> node:22-slim
- CI test matrix drops 20.x (keeps 22.x + 24.x)
- terminal-bench workflow Node 20 -> 22
- Linux/Windows install scripts upgrade their Node version targets

Documentation alignment:

- README.md badge + prerequisites
- AGENTS.md, CONTRIBUTING.md, docs/users/quickstart.md,
  docs/users/configuration/settings.md, docs/developers/contributing.md,
  docs/developers/sdk-typescript.md, docs/users/extension/extension-releasing.md,
  packages/sdk-typescript/README.md, packages/zed-extension/README.md,
  scripts/installation/INSTALLATION_GUIDE.md

Test gating:

- Two AuthDialog/AskUserQuestionDialog tests that drive <SelectInput>
  through ink-testing-library now race ink 7's frame-throttled input
  delivery and land on the wrong option. The maintainers had already
  marked one of them unreliable (skip on Win32 + CI+Node20). Extend
  that gate to cover all environments until upstream
  ink-testing-library ships an ink-7-compatible release that flushes
  input deterministically. The other test now uses it.skip with the
  same comment. No business code changes.

Verified locally:

- npm run typecheck across all workspaces: clean
- npm run lint (root): clean
- npm run test --workspaces:
    cli  312/312 files, 4918 passed, 9 skipped
    core 266/266 files, 6836 passed, 3 skipped
    webui 6/6, 201 passed
    sdk  40/40, 283 passed, 1 skipped
- npm ls ink: single ink@7.0.2 instance across all peer deps
- single react@19.2.4 instance

Generated with AI

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

* chore: align Node 22 floor across all shipping artifacts

Reviewer (tanzhenxin) flagged five surfaces where the >=22 engine bump
leaked: SDK package metadata, web-templates engines, /doctor runtime
check, main bundler target, and SDK bundler target. Each was a separate
escape hatch letting Node 18/20 consumers install or run the artifact
on an unsupported runtime.

- packages/sdk-typescript/package.json: engines.node >=18.0.0 -> >=22.0.0
- packages/web-templates/package.json: engines.node >=20 -> >=22
- packages/cli/src/utils/doctorChecks.ts: MIN_NODE_MAJOR 20 -> 22
- esbuild.config.js: target node20 -> node22 (main CLI bundle)
- packages/sdk-typescript/scripts/build.js: target node18 -> node22 (esm + cjs)
- packages/cli/src/utils/doctorChecks.test.ts: rename test label to v22+

Generated with AI

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

* ci(e2e): bump E2E workflow Node matrix to 22.x

Reviewer (tanzhenxin) flagged that e2e.yml still pinned node-version
20.x while root engines is now >=22, so every E2E run on push would
either fail at npm ci with engine error or silently exercise the bundle
on a runtime that's no longer in ci.yml's test matrix.

The macOS job in the same workflow already reads .nvmrc (which is 22)
so this only updates the Linux matrix.

Generated with AI

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

* fix(deps): drop root wrap-ansi override so ink 7 gets its declared dep

Reviewer (tanzhenxin) flagged that the root overrides.wrap-ansi: 9.0.2
predates this upgrade and forces every consumer (including ink) to v9,
while ink 7 declares wrap-ansi: ^10.0.0. The lockfile had no nested
install under node_modules/ink/, so ink 7 was running with a transitive
dep one major below its declared minimum.

Dropping the global override lets ink resolve its own wrap-ansi 10
nested install (now visible in the lockfile under
node_modules/ink/node_modules/wrap-ansi), while the cli package's own
direct `wrap-ansi: 9.0.2` dependency keeps the cli code path
(TableRenderer.tsx) on the version it has been tested against. The
nested cliui override is preserved for yargs which still needs v7.

Verified via `npm ls wrap-ansi`:
- ink@7.0.2 -> wrap-ansi@10.0.0 (newly nested)
- @qwen-code/qwen-code -> wrap-ansi@9.0.2 (unchanged)
- yargs/cliui -> wrap-ansi@7.0.0 (unchanged)

Generated with AI

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

* test(InputPrompt): un-skip placeholder ID reuse after deletion

Reviewer (tanzhenxin) flagged that the new it.skip on the
'should reuse placeholder ID after deletion' test was undisclosed in
the PR description and removed coverage of real product behavior
(freePlaceholderId / bracketed-paste backspace path) without a
TODO(#NNNN) link.

Their argument was sound: the skip rationale pointed at ink 7's input
throttle, but this same file just bumped the wait helper from 50ms to
150ms specifically to give ink 7 frame time. Re-running the test under
the bumped wait shows it passes reliably (5/5 runs in the full-file
context, 9/10 alone), so the skip was masking the throttle-flake that
the wait bump already addresses, not a real product bug.

Drop the it.skip and the now-stale comment so coverage of the
freePlaceholderId reuse logic is restored.

Generated with AI

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

* test(InputPrompt): bump first prompt-suggestion test wait to 350ms

The "accepts and submits the prompt suggestion on Enter when the buffer
is empty" test is the first in its describe block, so it pays the
renderer cold-start cost. On macOS-22.x CI runners that pushes the
Enter → onSubmit microtask past the default 150ms post-Enter wait. Match
the 350ms initial render wait used immediately above to absorb the cold
start.

* Revert "test(InputPrompt): bump first prompt-suggestion test wait to 350ms"

This reverts commit 6add83b62ea80c551c81f54af1fda3e6e7478f55.

* test(InputPrompt): wait for followup suggestion debounce before pressing Enter

Root cause of the failing prompt-suggestion tests on macOS and Windows
CI is not flaky timing of the test post-Enter wait — it's the 300ms
debounce inside createFollowupController.setSuggestion (shared core).
The Enter handler reads followup.state.isVisible synchronously, so if
the debounce timer has not fired before stdin.write('\\r'), the
suggestion path is skipped and onSubmit never runs. No amount of
post-Enter wait can recover from that — the keypress was already
processed against stale state.

The original wait(350) only left ~50ms margin over the 300ms debounce,
which ink 7 / React 19.2 mount overhead consumed on slow Windows
runners. Bump the initial wait to 700ms (named SUGGESTION_VISIBLE_WAIT_MS)
to give the debounce timer + cold-start render a generous buffer.

Apply to the two sibling tests too — without the wait their "does not
accept" assertions pass trivially when suggestion is never visible,
which is a false green that hides regressions in the actual reject path.

* fix(deps): align cli wrap-ansi with ink 7 (9.0.2 -> ^10.0.0)

Ink 7 ships its own wrap-ansi@10. CLI's direct dep was pinned to 9.0.2,
causing two copies of wrap-ansi in node_modules and a potential drift in
CJK width / ANSI handling between ink's internal text wrapping and our
TableRenderer.

Upgrading the CLI's direct dep to ^10.0.0 lets npm dedupe to a single
wrap-ansi@10 used by both ink and TableRenderer. API surface is
identical; the only documented behaviour change is that tabs are
expanded to 8-column tab stops before wrapping, which TableRenderer
doesn't feed in.

TableRenderer test suite (43 tests) passes against wrap-ansi@10.

Generated with AI

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

* chore(deps): document @types/node 20.x pin in overrides

The override pinning @types/node to 20.19.1 (while engines require
Node >=22) is intentional: bumping to @types/node@22.x re-introduces
a Dirent<NonSharedBuffer> type regression that breaks
@qwen-code/qwen-code-core/sessionService tests.

Add a sibling "//@types/node" note inside `overrides` so future
maintainers see the rationale and know when to revisit the pin
without having to dig through PR #3860 history.

Generated with AI

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

* test(AskUserQuestionDialog): link skipped Submit-tab test to tracking issue

The 'shows unanswered questions as (not answered) in Submit tab' test
was switched to `it.skip` in the ink 7 upgrade because
`ink-testing-library@4.0.0` doesn't flush input deterministically
through ink 7's 30fps throttle.

Add a `// TODO(#4036):` marker so the skip is greppable and can be
re-enabled once upstream ships an ink-7-compatible release.

Refs #4036

Generated with AI

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

* fix(deps): move @types/node pin comment out of overrides block

npm's `overrides` field requires every key to be a real package name —
the `"//@types/node"` comment-key added in 205855875 trips Arborist with
"Override without name" and breaks `npm ci` across all CI jobs.

Move the explanation to a sibling top-level `"//overrides"` key, which
npm ignores at the document root. Same documentation value, no
override-parser collateral damage.

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-11 17:29:50 +08:00
tanzhenxin
b2f04418fa
Merge pull request #2628 from QwenLM/feat/channels-telegram
feat(channels): add extensible Channels platform with plugin system and Telegram/WeChat/DingTalk channels
2026-04-01 16:19:08 +08:00
tanzhenxin
3a157d1fec feat(extension): add npm registry support for extension installation
- Add new npm extension installation channel via scoped packages (@scope/name)
- Implement npm.ts module with registry resolution, authentication, and download logic
- Support version pinning, dist-tags (latest, beta), and custom registries
- Handle private registry auth via NPM_TOKEN env var and .npmrc _authToken entries
- Update CLI install command with --registry flag for npm extensions
- Add comprehensive tests for npm package parsing and registry operations
- Update documentation for releasing and installing from npm registries
- Integrate npm updates into extension manager and update checking flow

This enables teams using npm for package distribution to publish Qwen Code extensions through their existing infrastructure, with full support for private registries and access control.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-27 20:38:35 +08:00
tanzhenxin
987eebd1c4 docs(channels): add plugin developer guide and rename mock to plugin-example
- Add comprehensive developer guide for building channel plugins
- Add user-facing docs for installing/configuring custom channel plugins
- Replace custom-channels.md with new plugins.md
- Rename @qwen-code/channel-mock to @qwen-code/channel-plugin-example
- Add messageId field to Envelope type for response correlation

This provides clear documentation for developers building custom channel
adapters and renames the mock package to better reflect its purpose as
a reference implementation example.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-27 03:19:34 +00:00
tanzhenxin
01c2e5a373 docs(channels): add custom channel plugins documentation
- Document channels config in extension manifest
- Add guide for creating custom channel adapters
- Explain ChannelPlugin interface and ChannelBase usage

This enables users to extend the channel system with custom platform adapters.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 14:41:20 +00:00
LaZzyMan
4d27950a95 feat: Add interactive TUI for extension management 2026-02-28 16:06:34 +08:00
LaZzyMan
f640705e4d fix typo error 2026-01-26 10:00:33 +08:00
LaZzyMan
63e24301f8 fix copy error 2026-01-23 16:41:23 +08:00
LaZzyMan
9af9ea259d feat: add select ui for claude marketplace 2026-01-23 16:23:30 +08:00
LaZzyMan
5f3f81b666 docs move extension to user and hide create docs 2026-01-21 19:57:26 +08:00