feat(plugins): show marketplace details and add an Update button#303
Merged
mbektas merged 2 commits intoMay 19, 2026
Merged
Conversation
The Plugins tab listed each configured plugin marketplace as just its name plus the source string. There was no surface for the description or version that the marketplace manifest already carries, no signal about how many plugins the marketplace offers, and no way to refresh a marketplace's cached manifest without removing and re-adding it. Enrich the marketplace list response with description, version, plugin_count, and plugin_names pulled from the cached ~/.claude/plugins/marketplaces/<name>/.claude-plugin/marketplace.json. Both the documented top-level fields and the legacy metadata.* fields are recognized for backward compatibility per the Claude Code marketplace docs. The manifest read is bounded by a 1 MiB size cap and the read failure path (OSError, ValueError, UnicodeDecodeError) is swallowed per entry so one bad manifest cannot abort the whole list. An empty-string CLI value (description: "", etc) falls through to the manifest value; a non-empty CLI value wins. Each marketplace row now shows the description (when present), a v<version> pill next to the name, and a plugin summary line of the form "N plugins: a, b, c, +D more" with the visible cap set so the row stays bounded for marketplaces with hundreds of plugins. A new Update button per row calls the new POST plugins/marketplace/<name>/update endpoint, which shells out to "claude plugin marketplace update <name>" and refreshes the panel on success. The endpoint inherits the existing @authenticated + claude_plugins_management policy gate, validates the name against the same path-traversal and flag-smuggling check used by marketplace remove, and injects a resolved GitHub token via env (never argv) so refreshing a private GHE marketplace works without process-level env. Tests cover: enrichment from both top-level and metadata.* fields, override-on-truthy semantics across all four enrichment keys, oversize-manifest rejection, unreadable-dir skip, missing-manifest skip, the new update CLI invocation, GitHub-token injection on update, path-traversal rejection on the name, marketplace row rendering (description / version / plugin summary / truncation / omitted-line on manifest-not-loaded), and the busy-state contract (both row buttons disable with verb-specific labels while an update is in flight). Closes plmbr#289.
…lass Second-pass review surfaced three items: 1. `_read_marketplace_manifest` did not validate the name against the path-traversal / NUL / leading-dash check that gates `list_marketplace_plugins`. The CLI sanitizes its own output, but leaving the manifest-read layer dependent on upstream gating is inconsistent. Validate at the manifest-read entry so the layer is self-defending; the enrichment caller in `list_marketplaces` already catches ValueError and skips, so a single bad CLI-returned name doesn't break the list. 2. `plugin_count` and `plugin_names` were merged independently. If a future CLI surfaced only the count, the manifest's names would be merged in, producing a row that reads "42 plugins: alpha, beta" -- the two halves disagreeing. Couple the pair: CLI sets both or the manifest sets both. 3. The Update button reused the `jp-mod-reject` class, the same destructive variant as Remove. Update is a benign refresh; switch to `jp-mod-accept` so the visual affordance matches the intent. New tests pin the coupling and the path-traversal rejection through the full `list_marketplaces` enrichment flow.
mbektas
approved these changes
May 19, 2026
pjdoland
added a commit
to pjdoland/notebook-intelligence
that referenced
this pull request
May 22, 2026
Promotes the [Unreleased] CHANGELOG snapshot to [5.0.0] - 2026-05-22 and expands it to cover everything merged into upstream/main after PR plmbr#287's docs refresh. Bumps package.json to 5.0.0. CHANGELOG additions cover the post-plmbr#287 surface: - Settings tabs: plugin marketplace picker (plmbr#284), plugin marketplace details + Update button (plmbr#303), per-workspace MCP disable (plmbr#286), JSON-paste path in Add MCP server (plmbr#285). - Launchers: hide-with-policy (plmbr#288), brand icons for Codex / opencode (plmbr#325, plmbr#333), per-launch directory picker (plmbr#332). - Chat sidebar and agentic UX: workspace @-mention in Claude mode (plmbr#327), reload-open-files-on-disk (plmbr#330), steered system prompt away from over-eager notebook creation (plmbr#336). - Skills: multi-manifest support (plmbr#321), tracks-upstream for user- imported skills (plmbr#322), HTTP kill switch for the reconciler (plmbr#291). - Accessibility: full sub-section covering plmbr#305-plmbr#320. - Security: shell-tool sandbox (plmbr#290), Claude UI-bridge sandbox (plmbr#323), 0o600 on encrypted token (plmbr#293), env-secret scrubbing (plmbr#295), MCP config shape validation (plmbr#299), XSS allowlist (plmbr#296), Copilot WS auth + origin (plmbr#301), GHE host detection (plmbr#292), fastmcp -> mcp SDK swap (plmbr#324). - Fixed: session listing unification (plmbr#310), session preview unwrap (plmbr#331), down-area runtime throw (plmbr#330 follow-up), WS message-handler leak (plmbr#294). - Removed: fastmcp dependency, history.jsonl session gate. Adds a Migration note covering the five behavior changes operators should review before upgrading from 4.x: fastmcp swap, path sandboxes, history.jsonl gate removal, workspace @-mention pointer shape, and the Copilot WebSocket auth/origin tightening. Two reviewer rounds (six personas each) applied: - Round 1 caught security overclaims (plmbr#293, plmbr#299, plmbr#323), the plmbr#284/plmbr#303 mis-attribution, missing migration note, 3 em dashes, and the stale `fastmcp==2.x.*` recommendation in the admin guide. - Round 2 caught the missing plmbr#301 migration bullet, missing version- matrix 5.0.x row, missing README TOC entry, and a couple of style nits (sub-heading overpromise, orphan bullet). Skipped (deferred to future PRs): - README first-run tour mention. - Admin guide HTTP kill-switch row in Failure-modes table. - Terminal drag-drop trust-model precision update after plmbr#327. - Cipher description nit in plmbr#293 (Fernet AES-128-CBC+HMAC, not AES-GCM).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Plugin marketplace rows on the Plugins tab now show the marketplace's description, version, and a plugin-count + comma-separated name list trimmed with
+N more. Each row has an Update button that runsclaude plugin marketplace update <name>and refreshes the panel.Solution
Backend (
notebook_intelligence/plugin_manager.py):list_marketplacesenriches each entry withdescription,version,plugin_count, andplugin_namespulled from the cached~/.claude/plugins/marketplaces/<name>/.claude-plugin/marketplace.json. Both the documented top-level fields (description,version) and the legacymetadata.description/metadata.versionshape are read for backward compatibility per the Claude Code marketplace docs. Newupdate_marketplace(name)method shells out toclaude plugin marketplace update <name>with optional GitHub-token injection via env (never argv).The manifest read is bounded by a 1 MiB size cap to keep a hostile or corrupt marketplace.json from OOMing the server on a list-marketplaces call. The read-failure path swallows
OSError(includingPermissionErrorandFileNotFoundError),ValueError, andUnicodeDecodeErrorso one bad manifest cannot abort the whole list endpoint._read_marketplace_manifestalso validates the marketplace name against the same path-traversal / NUL / leading-dash check used bylist_marketplace_plugins, so a CLI-returned name like../evilis rejected at the manifest-read layer rather than relying on upstream sanitization.Enrichment overlay rules:
descriptionandversion: a non-empty CLI value wins; an empty-string CLI value falls through to the manifest, so a future Claude CLI release that surfaces these fields server-side is honored.plugin_countandplugin_names: treated as a coupled pair. If the CLI surfaces either, both come from the CLI; otherwise both come from the manifest. Coupling prevents a row that shows "42 plugins: alpha, beta" because the count came from one source and the names from another.Handler (
notebook_intelligence/extension.py): newPluginsMarketplaceUpdateHandleratPOST plugins/marketplace/<name>/update, registered ahead of the catch-all detail route and inheritingPluginsBaseHandlerso@tornado.web.authenticatedand theclaude_plugins_managementpolicy gate both apply.Client (
src/api.ts):IPluginMarketplaceInfogains optionaldescription,version,plugin_count,plugin_names. NewupdatePluginMarketplace(name)method.UI (
src/components/plugins-panel.tsx+style/base.css): row renders the name with av<version>pill, the description (when present), the source string (existing), and a plugin summary "N plugins: a, b, c, +D more" via a new exportedsummarizePluginNameshelper. Visible cap defaults to 5; the row CSS also hastext-overflow: ellipsisas a width-based fallback. The plugin line is omitted entirely when the manifest hasn't been cached yet so a just-added marketplace doesn't render a misleading "no plugins." Buttons: Update (jp-mod-accept, matching its non-destructive intent) and Remove (jp-mod-reject). The busy state disables both buttons in tandem with verb-specific labels ("Updating…", "Removing…").Testing
tests/test_plugin_manager.py: pytest cases covering top-level + metadata.* enrichment, override-on-truthy semantics parametrized across description / version / plugin_count / plugin_names, plugin_count/plugin_names coupling (CLI sets one but not both → both still come from CLI), oversize-manifest rejection, unreadable-dir skip, missing-manifest skip, path-traversal rejection in a CLI-returned name, the new update CLI invocation, GitHub-token injection on update, and path-traversal rejection on the update name argument.tests/ts/plugins-panel.test.tsx: jest cases on the marketplace row (description / version / plugin summary, +N truncation, empty-state copy, manifest-not-loaded omission, update button calls API, error path) plussummarizePluginNamessemantics, plus the busy-state contract (both row buttons disabled, verb-specific labels) while an update is in flight.Risks / follow-ups
update_marketplaceinjectsGITHUB_TOKENunconditionally. The token never lands in argv (env only), and theclaudesubprocess only forwards it togitwhen the upstream is GitHub, so non-GitHub sources see an unused env var. Deliberate to avoid re-reading the source from disk on every update; comment in-tree calls this out.--scopeon update: marketplace names are unique within the global Claude plugin cache, soclaude plugin marketplace update <name>is scope-free by design.Closes #289.