mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-10 01:29:17 +00:00
* 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> |
||
|---|---|---|
| .. | ||
| design | ||
| developers | ||
| e2e-tests | ||
| plans | ||
| superpowers | ||
| users | ||
| verification/abort-controller-refactor | ||
| _meta.ts | ||
| declarative-agents-port.md | ||
| index.md | ||
| yaml-parser-replacement.md | ||