In-app marketplace search now uses the same autocomplete panel as the public
site so typing a query can pick plugins or templates without leaving the page.
The icon-only Guide control used by the Dify-embedded Marketplace header
now shows a "Guide" tooltip on hover and keyboard focus, with a matching
accessible name. Opening the docs dropdown is unchanged.
Clicking Install in the embedded plugin detail iframe starts the install
immediately and posts status back to the iframe, instead of opening a second
confirmation modal.
CMS/partner banner links only required a non-empty string, so
javascript: and other schemes could render on the console homepage.
Allow http(s) and same-origin relative paths only.
Publisher plugins and templates were capped at page 1 of 40, so larger
catalogs were silently truncated. Follow the API total through a small
page budget.
A timed-out or 500 publisher request was mapped to notFound(). Only a
successful empty creator response should 404; transport failures should
reach the route error boundary.
Unfiltered search fetched mixed-language pages then filtered in the
client, so totals and pageCount were wrong. Send the UI locale to the
API and keep client filtering only for collection previews.
Settle the wrapping carousel from resetting to idle with rAF plus a
short timeout fallback so loop clones unmount even when animation frames
are delayed.
Use shouldSearchMarketplacePlugins and HERO_GRID_PITCH_PX from production
catalog code, keep getStandaloneCreationHref exported via the marketplace
model entry, drop the test-only language helper, and remove an unused type import.
Un-export file-private creator-profile and banner-viewability symbols.
Restore model-provider hooks tests from origin/main, and mark the
standalone marketplace server helpers as knip entries so production
file checks stay green.
Restore the current skill/agent stack from origin/main after the merge, then fix marketplace lint, formatting, and type mismatches so Style Check can pass.
Keep the green blog label on one line with ellipsis on desktop, let
the title wrap, and drop the mobile Read more control now that the
whole stacked slide is the link.
Tuck the catalog tabs under the sticky search padding so the banner
gap stays, but search and Plugins/Templates tabs no longer add two
gaps on top of each other.
Give the standalone mobile sticky search a 16px padding-bottom and
shift catalog sticky offset by the same amount so banners do not
sit flush against the field.
Keep marketplace card-wrapper dialog and partner-only carousel autoplay.
Take main's skills permissions, skill card actions, and agent orchestrate
layout. Union oxlint suppressions from both sides.
The plugin catalog already used the mobile partner header grid; the
Templates tab still rendered Become a Partner on the description row.
Share the same header styles so both catalogs match.
Keep the standalone mobile search sticky below the header instead of
dropping sticky, and stop Chromium from scrolling the in-flow search box
into view when the desktop header search is focused or typed into.
Three independent defects made catalog search feel unstable.
Stale results flash on every search. `isSearchMode` was derived from the raw
nuqs `q` while the request body used the 500ms-debounced value, so keystroke #1
flipped into search mode with an empty query. That fired a full empty-query
search whose generic top-plugins list rendered for the debounce window before
the real results replaced it. Both now read the same debounced value.
Grid unmount on every query change. The infinite query had no
`placeholderData`, so `data` went undefined whenever the key changed and
ListWrapper's `!isLoading` gate tore the whole result grid out, collapsing the
container and jumping the scroll position. Measured against a live catalog, a
sort change blanked the grid for 152 of 360 sampled frames; with
`keepPreviousData` and an `isRefreshing` dim it is 0 of 461.
Requests that never settle. The Marketplace oRPC link called `globalThis.fetch`
with no deadline, so a stalled connection left the query pending forever with
no error state and no retry — the reported permanent spinner. Requests now
carry a 15s deadline composed with (never replacing) react-query's abort
signal. The server prefetch and the banner fetch get their own 2.5s budget:
awaiting them unbounded held the whole RSC response, pushing time-to-first-byte
to ~7s against a 3s-delayed API while the browser sat on the previous page.
Supporting changes:
- `getMarketplacePlugins` and `getMarketplaceCollectionsAndPlugins` no longer
swallow every rejection into a successful empty page. That turned outages
and aborted keystrokes into "no plugins found", suppressed retries, cached
the emptiness for the full staleTime, and killed `getNextPageParam`
permanently via `total: 0`. ListWrapper now renders a retry affordance.
- The collections fan-out is bounded to 4 concurrent requests instead of
firing one per collection at once, and per-collection failures degrade to an
empty carousel rather than a blank catalog.
- Scroll pagination measures once per frame instead of per scroll event. Each
threshold hit calls `fetchNextPage`, which defaults to `cancelRefetch: true`,
so an unthrottled burst aborted and restarted the in-flight page request —
and the backend counts those aborts against its search circuit breaker.
- Cross-page results are deduped by `org/name`; Meilisearch resolves
`install_count` ties by document order and the sync task rewrites those
documents every minute, so pages can overlap and collide on React keys.
- Search queries no longer inherit `retry: 3`; ~7s of backoff reads as a hang.