Skip to content

feat(plugins): show marketplace details and add an Update button#303

Merged
mbektas merged 2 commits into
plmbr:mainfrom
pjdoland:feat/289-marketplace-details-update
May 19, 2026
Merged

feat(plugins): show marketplace details and add an Update button#303
mbektas merged 2 commits into
plmbr:mainfrom
pjdoland:feat/289-marketplace-details-update

Conversation

@pjdoland
Copy link
Copy Markdown
Collaborator

@pjdoland pjdoland commented May 18, 2026

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 runs claude plugin marketplace update <name> and refreshes the panel.

Solution

Backend (notebook_intelligence/plugin_manager.py): list_marketplaces enriches each entry 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 (description, version) and the legacy metadata.description / metadata.version shape are read for backward compatibility per the Claude Code marketplace docs. New update_marketplace(name) method shells out to claude 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 (including PermissionError and FileNotFoundError), ValueError, and UnicodeDecodeError so one bad manifest cannot abort the whole list endpoint. _read_marketplace_manifest also validates the marketplace name against the same path-traversal / NUL / leading-dash check used by list_marketplace_plugins, so a CLI-returned name like ../evil is rejected at the manifest-read layer rather than relying on upstream sanitization.

Enrichment overlay rules:

  • description and version: 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_count and plugin_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): new PluginsMarketplaceUpdateHandler at POST plugins/marketplace/<name>/update, registered ahead of the catch-all detail route and inheriting PluginsBaseHandler so @tornado.web.authenticated and the claude_plugins_management policy gate both apply.

Client (src/api.ts): IPluginMarketplaceInfo gains optional description, version, plugin_count, plugin_names. New updatePluginMarketplace(name) method.

UI (src/components/plugins-panel.tsx + style/base.css): row renders the name with a v<version> pill, the description (when present), the source string (existing), and a plugin summary "N plugins: a, b, c, +D more" via a new exported summarizePluginNames helper. Visible cap defaults to 5; the row CSS also has text-overflow: ellipsis as 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) plus summarizePluginNames semantics, plus the busy-state contract (both row buttons disabled, verb-specific labels) while an update is in flight.
  • Full suites: pytest 788 pass, tsc clean, prettier clean, jest clean.

Risks / follow-ups

  • Behavior change: the marketplace list response gains four optional fields. Existing clients ignore them; the new client renders the richer row. No backwards-incompatible field renames.
  • Manual UI walk-through deferred: I couldn't start a local JupyterLab server autonomously (the auto-classifier blocked it). The jest suite covers the row layout, but a real-browser pass against a freshly-added marketplace (Playwright or hand) would catch CSS regressions the unit tests can't see.
  • Token injection on local sources: update_marketplace injects GITHUB_TOKEN unconditionally. The token never lands in argv (env only), and the claude subprocess only forwards it to git when 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.
  • No --scope on update: marketplace names are unique within the global Claude plugin cache, so claude plugin marketplace update <name> is scope-free by design.

Closes #289.

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.
@pjdoland pjdoland added the enhancement New feature or request label May 18, 2026
…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 mbektas merged commit 14fb168 into plmbr:main May 19, 2026
5 checks passed
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show more details on Plugin marketplaces and provide update option

2 participants