Plugin Marketplace (part 2)
The second half of the Plugin Marketplace page: what a plugin may read from your session history and boards, what it can watch you doing, ratings and reviews, the errors you can hit, the developer and publisher surfaces, package signing, and the registry and install internals.
What it is
This is part 2 of the Plugin Marketplace page. That page covers what the Marketplace is, where to find it, and the opening of the numbered walkthrough: browsing the catalogue, installing and uninstalling, the update check, version compatibility, the gate on third-party plugin backends, and an agent’s install request.
This half carries the deeper material: what a plugin may read from your own data and what it can watch you doing, ratings and reviews, the errors you can hit, the plugin-author surfaces, publishing and package signing, and how the registry, the install and the IPC layers actually work. The numbered walkthrough continues from step 9 below.
Where to find it
The same two surfaces as the main page: the Marketplace virtual project in the sidebar for browsing, and Settings → Plugins for what you have installed. Most of this half is reached from a single plugin’s own detail page or its Settings card, with the Plugin Developer view for the plugin authors themselves.
How it behaves
Your data a plugin can ask to read
Some plugins can ask to read your past Omniscio sessions and projects — for example, a plugin that turns your existing work into a summary or a deck. This is gated behind the sessions.readHistory permission and a strict, opt-in privacy model:
- Nothing by default. A plugin reads none of your history until you grant it. When a plugin needs access, a picker opens and you choose exactly which projects or sessions to hand over — it can only ever read those (a project you pick covers its sessions).
- Text only. The plugin receives the plain-text conversation, never the raw internals — tool output, file contents, and anything that could carry secrets are stripped out before it reaches the plugin.
- Logged and revocable. Every read is recorded in a log the plugin cannot touch, and your grant persists until you revoke it. Review what a plugin was granted, see its read log, and revoke access anytime under Settings → Plugins → "Session history access" (shown per plugin, only once a plugin has requested access).
Boards are a separate ask, on purpose. A plugin that wants to read your PM boards requests a
distinct boards.read permission rather than reusing the session-history one, because a board is
project-management data and not a conversation. Agreeing to share past sessions should never quietly
hand over something else. It follows the same model — nothing by default, you pick which boards, every
read logged, revocable — with two differences worth knowing: sharing one board shares only that
board (boards sit inside a workspace, and a workspace is far wider than the thing you pointed at),
and the plugin never receives the board's items, only a summary. Board grants appear in the same
per-plugin access panel as the others, each labelled for what it is.
A first-party built-in plugin shows no install-time consent dialog, so for those the picker and this access panel are the control surface. (Plugin authors: the developer side — the sessionHistory bridge namespace — is in Plugin Bridge Capabilities.)
Plugin authors: the marketplace publish validator keeps its OWN mirror of the declarable
permission set, because the cloud function cannot import host code. If the host gains a permission and
that mirror is not updated, an honest manifest declaring it is rejected at publish with
Unknown permissions: <name> — before a submission even exists, so no reviewer can wave it through.
That mirror is parity-locked to the host; see
marketplace-permission-parity-contract.md.
What a plugin can see you doing
Separate from reading your past work, a plugin can ask to see your sessions as they happen. Two different things, with two different answers:
- A companion or status overlay — a plugin that reacts to your work (a pet, an activity HUD)
can ask for
sessions.observeStatus, shown in the consent dialog as Session Activity. It sees only when a session starts, finishes, or needs you. It never sees a single word of what you or the agent wrote. A plugin that has not asked for it receives nothing at all, even if it has an overlay on screen. - A plugin watching a session it started itself — if a plugin launched a session for you, it can follow that session's output live. That needs no extra permission, because it is the plugin's own work: it can only ever see sessions it launched, never yours or another plugin's. If you revoke that project's folder access, the stream stops.
Rating
View ratings. Each detail page shows the average star rating, total review count, and a star-distribution histogram. The latest 20 reviews load below; the Load more button paginates 20 at a time. The star row under the plugin's name is that average, not your own rating — so when you are able to rate, the whole row becomes one control that jumps you down to the Ratings & Reviews form and puts the cursor in it. It stays a plain display when there is no form to reach (signed out, or the plugin is not installed), so it can never be a click that does nothing. It does not pre-pick a star: five stars that mean "the community said 4" in one place and "I say 4" in another would be two things wearing one face, and splitting the row into five buttons would turn a single spoken label into five for a screen reader.
Submit a rating (signed-in installers only). Submitting needs the marketplace GitHub sign-in —
MARKETPLACE_SUBMIT_RATINGrefuses unlessgetAdminStatus()isauthenticated— so the rating box (PluginRatingBox.tsx) re-checks that status every time the page opens and shows only what can actually work: It is not your Omniscio account: an earlier implementation sentauthService.getActiveAccountId()as anx-user-idheader, the endpoint ignored that header, and every submission failed — that code is gone.- Checking — nothing, until the status answers, so a stale value never flashes the form.
- Signed out (or the check failed) — builds with the in-app sign-in say to sign in with GitHub on the My Plugins tab; release builds, where that sign-in is compile-stripped, say reviews aren't available in this version yet. A release build holding an
amc-plugin logincredential counts as signed in (marketplace-admin-auth-contract.md I11, I12). The Ratings & Reviews card itself still renders either way, so a build that cannot submit says why instead of leaving a silent gap. - Signed in, not installed — "Install this plugin to leave a review."
- Signed in and installed — the star form, with the green Installed verified badge on the ratings header. Pick 1–5 stars, optionally add a short review (no links), click Submit Rating.
The server keys the rating to the verified marketplace Firebase uid from that GitHub sign-in (never your Omniscio or Anthropic account) and refuses a plugin's own author (
403, "You cannot review your own plugin").Edit your rating. The backend keys each rating by your identity and upserts — you have exactly one rating per plugin, and re-submitting replaces it rather than stacking a new one. When you've already rated a plugin this session the form is honest about that: it pre-fills your stars + review and the button reads Update your rating instead of Submit. The public ratings feed pseudonymizes rater ids (a review can't be traced back to "you" from the list), so this pre-fill is driven by a session-tracked map of your own submissions (
myRatingsin the marketplace store), cleared on cloud logout so a different signed-in user never inherits it.
Errors
- Registry can't be fetched — the card shows a red banner with the error text and a Retry button.
- Single install fails (network glitch, checksum mismatch, oversized file) — red toast quotes the reason; your existing install, if any, is left untouched (see "atomic" below).
- Rating submit fails — toast with the reason; nothing persists locally, and your typed review is kept so a retry does not start from blank.
- A rating the server REFUSES says which rule you hit. Reviewing a plugin you published (403
FORBIDDEN), the per-user rate limit, a link in the review text, an out-of-range star value — each comes back as a 4xx carrying the marketplace's own sentence, and that sentence is what you read: "You cannot review your own plugin", not a guess. It is carried as its own error type (MarketplaceRefusedError, thrown by marketplace-ratings.ts and passed through by TYPE in marketplace-user-errors.ts), the same way an upgrade-required refusal already was — because left to the classifier it arrives as the stringHTTP 403: {…}, matches the transport bucket's/\bhttp\s+\d{3}\b/, and becomes "Couldn't reach the plugin marketplace. Check your internet connection and try again." The marketplace had answered perfectly; the advice was wrong, retrying could never clear it, and the explanation was thrown away. Only a 4xx carrying a server sentence is passed through — a 5xx is a genuine upstream fault and keeps the transport copy, and a non-JSON, blank or over-long body falls back to it too. The raw status and body still reach the log.
A failure that retrying cannot fix says so. Raw causes are classified into plain sentences by toMarketplaceUserError (marketplace-user-errors.ts), and a deterministic failure never gets the generic "Please try again" — that advice sends the user round a loop that fails identically every time. A full disk asks them to free space; a directory another process holds open (EPERM/EBUSY, and safeSwapInstalledDir's own "currently in use" refusal) asks them to close what's using the plugin; and a plugins folder Omniscio cannot write to (EXDEV/EROFS) states plainly that retrying won't help and asks them to report it. These fs checks run before the corrupt-package check on purpose: an fs error quotes the path it failed on, so a locked …/manifest.json matches the corruption heuristic and would otherwise blame a package that was never damaged.
That last bucket also decides what reaches Sentry. A full disk or a locked file is the user's environment, so it logs WARN and is marked expected, like a network blip. Failing to write our own plugins folder is a defect, so it stays unmarked at ERROR — the EXDEV cross-device install bug produced no crash signal at all precisely because it hid in the unmarked generic bucket.
A reply bigger than Omniscio accepts says exactly that — never "corrupted". When the catalog, a plugin's details or ratings, or a package download is over its size cap (see "Fetch hardening" below), the user reads "The plugin marketplace sent more data than Omniscio can accept, so this couldn't be loaded. The plugin isn't damaged, and trying again won't help until we fix it on our end." — translated, and with no retry advice, because the same reply fails the same way until the server changes. This check runs before the corrupt-package check and matches only the size-cap errors themselves (Response too large / Registry response too large / Package too large), so an unrelated "too large" such as an HTTP 413 keeps its old bucket. It stays unmarked at ERROR: an oversized reply is our defect, and its Sentry report is how the 2026-09-23 incident was found — GitHub Integration's details had grown to 299,361 bytes and every user opening its page was told the plugin looked corrupted.
Developer dashboard
The Plugin Developer view (a separate virtual project for plugin authors) is where plugin authors track their own submissions to the marketplace. It shows:
- Sign-in button — OAuth flow against the marketplace Firebase project. Internal/dev builds only: the in-app GitHub sign-in handler is compile-stripped from the packaged app, so a release build shows a notice pointing at the CLI instead.
- Submissions list — each submission is an expandable status card (
SubmissionStatusCard). Collapsed, it shows plugin id + version, a statusPill(Pending review / Published / Changes requested), and the relative submission time. Expanded, it reveals a three-step status timeline (Submitted → In review → Decision, each marker coloured by state and stamped with a relative time), the full What's new changelog, Reviewer notes (shown for an approved submission with feedback as well as a rejection), and a what-happens-next hint tailored to the status.
In a release build, the tab signs in with your amc-plugin login. A packaged app has no marketplace sign-in of its own, so it borrows the credential amc-plugin login already wrote to ~/.amc/marketplace-token — the same account that ran amc-plugin publish, so everyone who has a submission to show already has this file. Nothing new to sign into: the My Plugins tab lists your submissions as soon as the CLI is logged in, amc-plugin logout signs the tab out too (a refreshed token is written back to the CLI's file, never copied into the app), and a borrowed login is always treated as a developer, never an admin — it can list your own submissions but cannot open any review surface. The app's own token, when one exists (internal builds), still wins (marketplace-cli-token.ts; the borrowed-CLI-credential rule in marketplace-admin-auth-contract.md).
The view exists separately from the Marketplace Review view used by Omniscio admins to triage incoming submissions.
For agents
Who may submit (the publisher gate)
Signing in is not enough to publish. Uploading is refused unless the account is an approved publisher. Being signed in only proves you hold a valid login; before this gate existed, any GitHub account could put third-party code into the admin review queue, and the review queue was the only thing between a stranger and users.
- Refusal is a 403 with a plain-English message, raised before the package is parsed — an unapproved account never gets its upload read or stored.
- A first refusal records an application. The developer's real identity (GitHub handle, display name, email) is written once so an admin can see who is asking; repeated attempts after that cost nothing and write nothing.
- Admins always pass. A
role: 'admin'account publishes regardless of its publisher status — the floor that makes the gate safe to roll out in any order. - The same gate covers automations.
uploadAutomationaccepts a definition rather than a zip, but it is third-party code by another name and carries the identical check — see automation-marketplace.md. - Blocking someone does not unpublish their existing plugins. It stops future submissions only; takedown is the separate unapprove action.
Admins manage this through two admin-only endpoints — getPublishers (who is waiting) and setPublisherStatus (approve / block). There is no in-app screen for it yet; approving a publisher is an API call today. Full invariants: marketplace-publisher-gate-contract.md.
Package signing (built, not yet switched on)
Every published package is meant to carry an Ed25519 signature so the app can prove the file it downloaded is the one that was reviewed. The machinery exists on both sides — the server signs at upload, the catalogue serves the signature, and the app knows how to verify it, including key rotation. It is not switched on yet: no signing key exists, so nothing is signed today.
- A deploy without the key now FAILS. The signing secrets are declared and bound to the upload function, so signing can no longer be "switched on" by accident and silently do nothing — which is exactly what would have happened before, because nothing ever fed the key to the function.
- An upload that cannot be signed is refused, with a retryable error, before anything is stored — no half-uploaded file, no reserved version left behind.
- Approving a plugin can no longer un-sign it. The publish step rewrites the version record wholesale, so it now carries an existing signature forward.
- Already-published packages are signed by a script, not by re-uploading. It signs only bytes it can verify against the checksum recorded at publish time, and refuses anything it cannot prove.
Turning it on has a strict order, and getting it wrong breaks installs: bind (done) → create the key → deploy → back-sign the catalogue → pin the public key in a client release → wait for the fleet to upgrade → only then enforce. Enforcing before the fleet has upgraded breaks every install on every un-upgraded client.
Known gap: 5 published versions have no recorded checksum in any record, so their bytes cannot be proven and they will not be signed. (Two more that looked unrecoverable turned out to still have their checksum on the original submission — the same hash written at upload, simply never copied across — and are now recovered legitimately.)
Of those 5, only two need action: repoguard and virtual-pets are the only version of their plugin, so they would become uninstallable once strict checking is on. The rest are superseded by later versions, or retired.
Re-publishing those two is blocked by code, not by their authors — both are recorded as owned by a placeholder account nobody can sign in as, so any real developer's upload is refused, and the version has to be bumped because the endpoint will not overwrite one that already exists. Existing installs are unaffected either way: strict checking only applies when installing or updating.
Full invariants and current counts: marketplace-package-signing-contract.md.
Promotional films on the store website
A listing can open with a film. On the public Marketplace website's plugin page, a listing that has a film shows a Preview section first, above Screenshots: a still of the film's own start card, with a ▶ Watch the preview button in a bar below it. Clicking opens the film full screen in a new tab. Nothing plays on the page, and the store never embeds the film — the shares service refuses to be framed by other sites on purpose. With no film the page is unchanged; a film with no still (or a still that fails to load) shows a plain gradient card with a play mark, and it still works as a labelled button. Only what visitors read says "Preview" — the film field, plugin.film and the owner command keep their names. The in-app Marketplace page does not show films yet.
Each film's still is one of the store's own images. It lives at firebase/marketplace/public/assets/film-stills/<id>.webp, named after the film link's <id>, and ships with the store website. To give a new film its still: open https://shares.omniscio.com/s/<id>/run (the film without the share page's buttons) in a 1440×756 browser window, capture the start card, save it as WebP under that name, and ship it in a store PR — it goes live with the store deploy. Until then the card shows the gradient. Pointing a listing at a new film link never shows the old film's still, because the name no longer matches.
Only Omniscio share links are accepted. A film must be https://shares.omniscio.com/s/<id>, where <id> is lowercase letters, digits and hyphens (up to 80). Anything else is refused when it is set, dropped by the store API when it is read, and never shown by the website; tracking parameters and fragments are stripped.
Two ways to set it — the most recent one stands.
- Store owners (Google Cloud access to the marketplace project) use the owner command from
firebase/marketplace, no plugin release needed. It previews by default and writes only with--apply:node scripts/set-plugin-film.mjs <plugin-id> https://shares.omniscio.com/s/<id> # dry run: shows old → new node scripts/set-plugin-film.mjs <plugin-id> https://shares.omniscio.com/s/<id> --apply # sets it node scripts/set-plugin-film.mjs <plugin-id> --clear --apply # removes it - Developers add
"film": "https://shares.omniscio.com/s/<id>"to thepluginblock ofmanifest.json; it takes effect when that release is approved."film": ""removes it, and leaving the key out keeps whatever film the listing already has. An invalid value fails the upload with a message naming the rule.
Where it is served from. The film travels in the film field of GET /getPluginDetail/<pluginId> ('' when there is none). The live store API is amc-back, whose code now lives in this repo and carries the same film change; amc-back is deployed by hand, so a film appears on the live website only after both the store deploy and an amc-back deploy. Full rules: marketplace-listing-film-contract.md.
How it works
Registry source. The catalog is served by AMC's own backend at https://amcback.jls.dev/marketplace with three GET endpoints (it ran as Firebase Cloud Functions until 2026-07-31; every endpoint kept its exact name as a path segment, so only the base URL changed). The Cloud Functions are not retired: the hosted sign-in page deliberately still calls them, because amc-back's CORS preflight returns no Access-Control-Allow-Origin and a browser cannot reach that mount (contract). The three GET endpoints:
GET /getRegistry— full plugin catalogGET /getPluginDetail/<pluginId>— single-plugin metadata (changelog, permissions, etc.). Its version entries deliberately carry nochecksums— installs verify against the registry, and shipping every version's per-file map is what pushed a reply past the client's cap (contractdetail-reply-carries-no-checksums)GET /downloadPlugin/<pluginId>/<version>— signed-URL redirect to the.amcpluginpackageGET /getRatings?pluginId=<id>&limit=<n>&offset=<n>— paginated user reviewsPOST /submitRating(requiresx-user-idheader) — submit or replace a rating
The registry response shape is { plugins: MarketplacePlugin[], generatedAt: string }. The cloud function enforces PluginCategory at publish-time so the renderer can treat the wire shape as MarketplacePlugin 1:1.
Auth pipeline (developer + rating endpoints). OAuth flows through the marketplace Firebase project (auth.<...>.firebaseapp.com callback). The MARKETPLACE_ADMIN_AUTH IPC handler kicks off the flow; on return the desktop client redeems the completed sign-in by polling getAuthSession with the session id in the X-Auth-Session header — never a query-string param, because that id is a bearer capability and a URL lands it in access logs. The response carries a refreshToken so the long-lived token can be persisted. The credential doc itself (auth_sessions/<id>) is locked allow read, write: if false, so getAuthSession is the only sanctioned redemption route — reading it directly returns 403 on every poll (the 2026-08-19 sign-in outage). See marketplace-admin-auth-contract.md. For rating submission, the server keys the rating doc and its per-user rate limit to the verified Firebase uid it decodes from the Bearer token, and deliberately ignores any client-supplied identity header (anti-fraud). An earlier client sent the active Omniscio account id as an x-user-id header; the endpoint discarded it, so every rating 401'd. Submission therefore needs the same marketplace GitHub token the developer endpoints use, and because MARKETPLACE_ADMIN_AUTH is registered only under INTERNAL_ONLY it does not exist in a packaged build. That is why the submission UI is gated on MARKETPLACE_DEVELOPER_AUTH_AVAILABLE, locked by marketplace-admin-auth-build-gate.test.ts.
Fetch hardening. Every HTTP call goes through fetchWithTimeout() (src/main/services/marketplace/marketplace-service.ts) which enforces a 15-second timeout (AbortController), checks Content-Length against the size cap before the body streams, and re-checks the actual byte count after download (catches chunked transfers that omit Content-Length). The registry, plugin-detail and ratings responses are each capped at 256 KB; each plugin package at 10 MB. If the function returns HTML instead of JSON (rate-limited, deploy in progress), Omniscio catches the JSON parse error and surfaces a useful preview like Registry response is not valid JSON: "<!doctype html>…" instead of a cryptic SyntaxError.
Checksum verification. When a plugin's registry entry includes a checksums map (the only mode the official registry uses), Omniscio verifies it before writing the directory to disk. The map holds two kinds of entries: a per-file hash for each file inside the .amcplugin package (verified against the extracted file), and an optional package-level entry under the well-known key package.sha256 — the SHA-256 of the entire downloaded package, verified against the whole download rather than treated as a file inside the zip. A mismatch of either kind aborts the install with Checksum mismatch for <file>: expected abc123…, got def456…; a per-file entry the package omits aborts with Checksum file missing: <file> (so a compromised registry can't claim a file the bundle doesn't contain). Your existing install, if any, is untouched because the install was still in the temp directory. (package.sha256 is a package-level fingerprint, not a file in the zip — mis-handling it as a missing file was the 2026-06-24 install-failure bug; see marketplace-checksum-contract.md.)
Atomic install. The .amcplugin package (a zip archive) downloads into <userData>/.plugin-staging/<id>/, gets extracted there, has its checksums verified, and only then is atomically renamed into place at <userData>/plugins/<id>/. If an existing install is being updated, Omniscio removes it (rmSync recursive) immediately before the rename. On any failure mid-install, the staging directory is cleaned up best-effort and your previous install is preserved.
Staging deliberately lives under the user-data dir, not os.tmpdir() — and as a sibling of plugins/, never inside it. The commit step is a rename(), which cannot cross filesystems, so staging on the system drive broke every install and update with EXDEV: cross-device link not permitted on any machine whose user-data dir sits on a different drive (a data dir relocated to H:\ while TEMP stays on C:\). Because the swap aborts cleanly, the symptom was an update that failed with "Something went wrong with the plugin marketplace" while the old version stayed installed — for every plugin, not one. Keeping staging a sibling matters too: a staging dir holds a real manifest.json, so nested under plugins/ a leftover would be counted as an installed plugin and then removed by the registry revocation sweep. The sweep now also requires a directory's NAME to be a legal plugin id, which catches a differently-named leftover such as an update backup — but that is not a substitute for the sibling rule, because a staging dir is named for the plugin id itself and so would still pass the name check. Keep staging outside plugins/.
Cache. The registry response is cached in-memory for 5 minutes (CACHE_TTL_MS) to avoid hammering Firebase when you open and close the marketplace. Install and uninstall both invalidate the cache so the very next fetch reflects the new state. Push event PLUGIN_REGISTRY_CHANGED (with { pluginId, installed }) is emitted on every install/uninstall, alongside PROJECTS_CHANGED and DIVIDERS_CHANGED so the sidebar reflects the new virtual project immediately.
IPC surface. Seven handlers expose the service to the renderer:
MARKETPLACE_FETCH_REGISTRY— returnsMarketplacePluginStatus { plugin, installed, installedVersion, updateAvailable }[]MARKETPLACE_INSTALL— install or update bypluginId; also adds toenabledPluginsand restores/creates the virtual projectMARKETPLACE_UNINSTALL— remove the plugin's directory bypluginId; soft-deletes the virtual project, preserves plugin SQLite dataMARKETPLACE_GET_PLUGIN_DETAIL— single-plugin detail (changelog, permissions, full description)MARKETPLACE_GET_RATINGS— paginated reviews ({ pluginId, limit?, offset? })MARKETPLACE_SUBMIT_RATING— submit or replace your rating ({ pluginId, stars, review? }); refused unless the marketplace GitHub sign-in isauthenticated, and the server keys the rating to that verified Firebase uidMARKETPLACE_GET_MY_PLUGINS— the signed-in developer's own submissions. The handler first checksgetAdminStatus(); only whenstatus === 'authenticated'does it callfetchMySubmissions()(marketplace-review-service.ts), which hits the auth-scoped/getMyPluginscloud function (developer.uid == request.auth.uid) with the OAuth bearer token. A signed-out developer gets a benign[]without a network call — the renderer decides whether to show the "Sign in with GitHub" prompt fromMARKETPLACE_ADMIN_STATUS, so the empty array is never mistaken for an authenticated-but-empty result. That status channel is registered in the same shipped marketplace-handlers.ts, not in the compile-stripped review handlers — while it lived there, a release build's tab could never see itself as signed in, whatever identity the main process held. Each row is aMySubmission(types-marketplace-review.ts) — a lean subset of the reviewer'sReviewSubmissionenriched withchangelog,packageSize,reviewedAt, andreviewNotes. The My Plugins tab renders these throughSubmissionStatusCard(SubmissionStatusCard.tsx) — an expandable status-detail card. The collapsed header carries plugin id + version, a statusPill, and a relative submitted time; expanding reveals a status timeline, the full changelog, reviewer notes (surfaced for an approved-with-feedback submission too, not just rejections), and a status-specific next-step hint. The pure timeline/step logic lives in submission-status.ts (getSubmissionSteps/shouldShowReviewNote), unit-tested in marketplace-submission-status.test.ts.
The admin-side review handlers (approve/reject/sandbox/claim/source-tree, 11 total) live in src/main/ipc/marketplace-review-handlers.ts.
Files.
- Backend service — src/main/services/marketplace/marketplace-service.ts (fetch, install, uninstall, detail, ratings, cache)
- IPC handlers — src/main/ipc/marketplace-handlers.ts
- IPC channel names — src/shared/ipc-channels/index.ts (
MARKETPLACE_*) - Marketplace grid view — src/renderer/src/features/marketplace/MarketplaceView.tsx (Browse-tab update badge + banner; launch/interval/focus check lives in App.tsx)
- Updates Review modal — src/renderer/src/features/marketplace/PluginUpdatesModal.tsx
- Permission-diff helper — src/shared/plugin-permissions.ts (
diffPermissions) - Trust-signal helper — src/shared/marketplace-trust-signals.ts (
isFirstPartyPlugin,summarizePluginTrust,SENSITIVE_PLUGIN_PERMISSIONS) - Plugin card — src/renderer/src/features/marketplace/MarketplacePluginCard.tsx
- Plugin detail page — src/renderer/src/features/marketplace/MarketplacePluginDetail.tsx
- Shared re-approval callout — src/renderer/src/components/ui/ReconsentCallout.tsx (Settings + card + detail; see plugin-reconsent-surfacing-contract.md)
- "Opened a dead plugin" toast — src/renderer/src/features/plugins/usePluginReconsentToast.ts (fired from Dashboard.tsx)
- Marketplace sidebar — src/renderer/src/features/marketplace/MarketplaceSidebar.tsx (the SOLE home of the category filter) plus its footer — MarketplaceSidebarFooter.tsx (the four-state footer: installed count, status line + freshness stamp, the empty-state Browse plugins link, and the on-demand Check for updates button). The footer's success-only pulse (
.marketplace-check-flash) is defined in globals.css. - Browse toolbar + plugin grid — src/renderer/src/features/marketplace/BrowseTab.tsx (the SOLE home of search + sort; the search box is the shared
SearchInput, deliberately — see "One control, one home" above) - Developer dashboard — src/renderer/src/features/plugin-developer/PluginDeveloperView.tsx
- Marketplace store — src/renderer/src/stores/marketplace-store.ts
- Developer store — src/renderer/src/stores/plugin-developer-store.ts
- Types —
MarketplacePlugin,MarketplacePluginStatus,PluginCategoryin src/shared/types.ts;MarketplacePluginDetail,MarketplaceRatingsResponsein src/shared/types-marketplace.ts - Legacy Settings card (browse-only) — src/renderer/src/features/settings/PluginSettings.tsx
Related
Plugin Marketplace is the first half of this page — what the Marketplace is, how to browse it, and how installing, updating and the security gate work. The contracts and key files listed above hold the invariants behind what this half describes.
Last verified 2026-10-01