Skip to content

fix(plugins): accelerate marketplace loading and harden install state - #2549

Merged
qdaxb merged 4 commits into
wecode-ai:mainfrom
luckjun529-lang:bugfix/plugin-marketplace
Aug 11, 2026
Merged

qdaxb merged 4 commits into
wecode-ai:mainfrom
luckjun529-lang:bugfix/plugin-marketplace

Conversation

@luckjun529-lang

@luckjun529-lang luckjun529-lang commented Aug 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Speed up Wework plugin marketplace first paint: durable peek, progressive catalog merge, disk-backed personal marketplace listing, and batched backend access checks (avoid N+1).
  • Harden install/detail consistency: account pluginId drives marketplace card installed, installed-strip opens enriched marketplace detail, and cloud installs paint before slow Codex plugin/list.
  • Fix review regressions: paint installed strip even when catalog is still empty; keep plugin/installed refreshes in memory only so they do not clobber the durable readState snapshot.

Test plan

  • Open Plugins marketplace cold: OpenAI/personal tabs paint from peek/disk without waiting ~10s on plugin/list
  • With cloud plugins already installed: installed strip shows before marketplace catalog finishes
  • Install a cloud plugin: card switches to installed; strip and detail stay consistent
  • Open an installed-strip item: detail uses marketplace-enriched content when available
  • Composer plugin picker still lists apps without blocking on plugin/list
  • pnpm --filter wework exec vitest run on marketplace-related tests
  • cd backend && uv run pytest tests/services/test_plugin_marketplace_v2.py

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Personal marketplace plugins now appear directly from local storage with installation status and metadata.
    • Marketplace results load progressively from local, installed, and cloud sources.
    • Search ranks matches across names, descriptions, authors, developers, and tags.
    • Installed plugins more reliably link to marketplace details.
    • Plugin data loads faster with caching, background refresh, and request deduplication.
  • Bug Fixes

    • Improved handling of duplicate personal marketplace entries and official marketplace identification.
    • Marketplace installation states now remain accurate while installs or authorization are in progress.
    • Composer warnings now include plugin names, enabled status, and installation state.

junlong5 and others added 3 commits August 10, 2026 18:59
Avoid blocking chat on Codex plugin/list, load personal catalogs from disk,
and keep the expanded plugin preview chip during conversations.

Co-authored-by: Cursor <cursoragent@cursor.com>
Refresh installed membership when peek is stale, overlay catalog install
flags from account installs, open installed-strip details via marketplace
enrichment, and paint cloud installs without waiting on plugin/list.

Co-authored-by: Cursor <cursoragent@cursor.com>
…lobber

Keep cloud installs visible when marketplace rows are still empty, and do not persist membership-only plugin/installed refreshes into the 7-day readState cache.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request batches backend plugin access checks and adds disk-based personal marketplace discovery. It introduces durable Codex plugin read-state caching, progressive marketplace loading, catalog merging, client-side search ranking, installed-plugin matching, and related UI and test updates.

Changes

Marketplace access and local catalog

Layer / File(s) Summary
Batched plugin access checks
backend/app/services/plugin_marketplace_service.py, backend/tests/services/test_plugin_marketplace_v2.py
Marketplace listing preloads grants and namespace access data, reuses that context during checks, and tests consolidated resource-member lookups.
Disk marketplace discovery and API wiring
wework/src-tauri/src/local_executor.rs, wework/src-tauri/src/lib.rs, wework/src/api/local/codexPlugins.ts, wework/src/api/local/listPersonalMarketplaceFromDisk.test.ts
The local executor discovers personal plugins from disk and Codex caches. Tauri and the local API expose the resulting marketplace items.
Cached local plugin state
wework/src/api/local/codexPlugins.ts, wework/src/api/local/codexPlugins.readStateCache.test.ts, wework/src/features/workbench/WorkbenchProvider.tsx
Codex read state now supports durable caching, TTLs, stale refresh, request deduplication, warmup, and direct installed-plugin loading.
Catalog merge and matching
wework/src/components/plugins/marketplaceCatalogMerge.ts, wework/src/components/plugins/findMarketplaceItemForInstalled.ts, wework/src/features/plugins/marketplaceSearch.ts, wework/src/features/plugins/marketplaceIdentity.ts, wework/src/components/plugins/*test.ts
Local and disk marketplace rows merge with installation state. Personal aliases deduplicate. Search ranking and installed-plugin catalog matching are added.
Progressive workspace loading
wework/src/components/plugins/PluginsWorkspace.tsx, wework/src/components/plugins/PluginsWorkspace.test.tsx, wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.ts, wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.test.ts, wework/src/components/chat/composer/*, wework/src/components/layout/DesktopWorkbenchMain.tsx, wework/src/features/plugins/loadComposerPluginApps.ts, wework/e2e/desktop/task-flow.e2e.mjs
The workspace paints local, disk, cloud, and installed data as each source resolves. Search runs over cached catalog data, installed details use catalog matches, and installation state remains stable during pending operations.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: ci:all

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary changes: faster marketplace loading and more reliable plugin installation state.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (12)
backend/tests/services/test_plugin_marketplace_v2.py (1)

974-1010: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Exercise the namespace-grant batch path.

This fixture creates only entity_type="user" grants. It does not execute the new granted-namespace name query. A per-plugin namespace lookup can regress while this test passes.

Add an approved namespace membership and a namespace plugin grant. Assert that listing returns the expected item and keeps namespace-name resolution batched.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/services/test_plugin_marketplace_v2.py` around lines 974 -
1010, Extend the test setup around PluginMarketplaceService.list_plugins to
create an approved namespace membership and a corresponding namespace plugin
grant, then assert the expected plugin is returned. Update the query-count
assertion to cover the namespace-name resolution path and verify it remains
batched rather than issuing per-plugin lookups.
wework/src/features/plugins/loadComposerPluginApps.ts (1)

77-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

pluginStates supersedes pluginKeys.

Each entry of pluginStates already carries the same key value that pluginKeys reports, computed with the identical expression. Drop pluginKeys to remove the duplicated expression and keep the diagnostic payload smaller.

♻️ Proposed change
       localCount: localItems.length,
-      pluginKeys: installedPlugins.map(
-        plugin => plugin.spec.source?.pluginKey || plugin.metadata.name
-      ),
       pluginStates: installedPlugins.map(plugin => ({
         key: plugin.spec.source?.pluginKey || plugin.metadata.name,
         enabled: plugin.spec.enabled,
         installState: plugin.spec.installState,
       })),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/features/plugins/loadComposerPluginApps.ts` around lines 77 - 85,
Remove the redundant pluginKeys mapping from the diagnostic payload in
loadComposerPluginApps, since pluginStates already provides the identical key
values. Preserve pluginStates and its existing key, enabled, and installState
fields unchanged.
wework/src/components/plugins/PluginsWorkspace.tsx (3)

2603-2626: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the redundant rejection guard.

At Line 2607, cloudItems is [] whenever cloudResult.status === 'rejected'. The second clause at Line 2610 is therefore always true, and the condition reduces to a plain rejected check.

♻️ Proposed change
-        if (cloudResult.status === 'rejected' && cloudItems.length === 0) return
+        if (cloudResult.status === 'rejected') return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/plugins/PluginsWorkspace.tsx` around lines 2603 - 2626,
In the Promise.allSettled callback, simplify the cloudResult guard by removing
the redundant cloudItems.length check and return directly when
cloudResult.status is 'rejected'. Preserve the existing fulfilled-result
handling and partial catalog painting flow.

2396-2511: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Extract the repeated catalog merge pipeline into one helper.

The same sequence now exists three times in this effect:

  • applyCatalogSnapshot at Line 2333-2350
  • paintPartialCatalog at Line 2460-2489
  • the post-settlement disk branch at Line 2530-2554

Each block resolves cloud rows from cache, calls mergeDiskPersonalIntoLocalRows, calls mergeInstalledPlugins, then calls mergeMarketplaceCatalog. The three copies already differ in how they source the previous installed list, which is where drift will start.

paintPartialCatalog is about 116 lines and the enclosing effect is about 466 lines. Extract a single buildCatalogSnapshot({ cloudItems, cloudInstalled, localState, diskPersonalItems }) helper outside the component and reuse it in all three places.

As per coding guidelines: "functions should remain focused, preferably under 50 lines" and "Before adding code, search for and reuse existing components, services, utilities, and patterns; extract shared logic instead of duplicating it".

Also applies to: 2520-2569

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/plugins/PluginsWorkspace.tsx` around lines 2396 - 2511,
Extract the shared catalog-merging pipeline into a focused
buildCatalogSnapshot({ cloudItems, cloudInstalled, localState, diskPersonalItems
}) helper outside the component. Move cache fallback resolution,
mergeDiskPersonalIntoLocalRows, mergeInstalledPlugins, and
mergeMarketplaceCatalog into that helper, while preserving each caller’s
existing previous-installed source and inputs. Replace the duplicated logic in
applyCatalogSnapshot, paintPartialCatalog, and the post-settlement disk branch
with calls to the helper.

Source: Coding guidelines


2339-2345: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the diskPersonalItemsForMerge declaration above applyCatalogSnapshot.

applyCatalogSnapshot is defined at Line 2326 and reads diskPersonalItemsForMerge, but that variable is declared with let at Line 2394. The current call sites are async, so no temporal dead zone occurs today. A future synchronous call before Line 2394 would throw a ReferenceError. Declare the four merge accumulators before applyCatalogSnapshot to remove the forward reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/plugins/PluginsWorkspace.tsx` around lines 2339 - 2345,
Move the declaration of diskPersonalItemsForMerge and the other three merge
accumulators above applyCatalogSnapshot, preserving their existing let
declarations and initialization. Ensure applyCatalogSnapshot can safely
reference all four variables without relying on asynchronous call sites.
wework/src/components/plugins/PluginsWorkspace.test.tsx (1)

2489-2528: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use one pattern to wrap the existing fetch mock.

Line 2518 captures vi.mocked(fetch) and calls it directly. Line 3040 captures vi.mocked(fetch).getMockImplementation() and calls the implementation. Both work, but the two patterns describe the same intent in the same file. Pick one, preferably the getMockImplementation() form, so vi.mocked(fetch).mock.calls keeps recording every request in both tests.

Also applies to: 3036-3047

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/plugins/PluginsWorkspace.test.tsx` around lines 2489 -
2528, Use the same fetch-wrapper pattern in both tests: update the local fetch
mock around the test beginning “paints local Codex plugins…” and the
corresponding wrapper near the later test to obtain and call fetch’s existing
mock implementation via vi.mocked(fetch).getMockImplementation(). Preserve
marketplace request interception while ensuring all delegated requests continue
through the mock so vi.mocked(fetch).mock.calls records them consistently.
wework/src/api/local/listPersonalMarketplaceFromDisk.test.ts (1)

26-105: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add coverage for the failure path.

listPersonalMarketplacePluginsFromDisk catches an invoke rejection and returns an empty array. That branch protects the personal tab from a broken Tauri command and is not covered. Add a case where mocks.invoke.mockRejectedValue(...) and assert the result is [].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/api/local/listPersonalMarketplaceFromDisk.test.ts` around lines 26
- 105, Add a failure-path test to the listPersonalMarketplacePluginsFromDisk
suite that configures mocks.invoke.mockRejectedValue(...), calls
listPersonalMarketplacePluginsFromDisk, and asserts it returns an empty array.
Keep the existing setup and success-path assertions unchanged.
wework/src-tauri/src/local_executor.rs (2)

3844-3867: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

latest_cached_personal_plugin_root selects by modification time, not by version.

Codex cache directories are named by version (for example 0.1.0). This function returns the directory with the newest mtime. A re-extracted or touched older version then wins over the newest installed version. Consider comparing the directory names as versions and using mtime only as a tie-break.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src-tauri/src/local_executor.rs` around lines 3844 - 3867, The
latest_cached_personal_plugin_root function should select the highest semantic
version from valid cache directory names instead of prioritizing modification
time. Parse each directory name as a version, ignore unparseable names, and use
modified time only when versions tie; preserve manifest validation and return
the selected directory path.

3831-3841: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

The installed-tree preference check ignores Claude manifests.

Every other manifest probe in this file accepts .codex-plugin/plugin.json or .claude-plugin/plugin.json. This check only tests .codex-plugin/plugin.json. A marketplace entry whose tree contains only a Claude manifest is treated as a stub, so plugin_path is replaced by the cached copy.

♻️ Proposed fix
     if candidate.installed {
         existing.installed = true;
         // Prefer the concrete installed tree when marketplace.json only had a stub.
-        if existing.plugin_path.is_empty()
-            || !Path::new(&existing.plugin_path)
-                .join(".codex-plugin/plugin.json")
-                .is_file()
-        {
+        let existing_root = Path::new(&existing.plugin_path);
+        let existing_has_manifest = !existing.plugin_path.is_empty()
+            && (existing_root.join(".codex-plugin/plugin.json").is_file()
+                || existing_root.join(".claude-plugin/plugin.json").is_file());
+        if !existing_has_manifest {
             existing.plugin_path = candidate.plugin_path;
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src-tauri/src/local_executor.rs` around lines 3831 - 3841, The
installed-tree preference check in the candidate merge logic must recognize both
manifest locations. Update the condition guarding existing.plugin_path so it
treats either .codex-plugin/plugin.json or .claude-plugin/plugin.json as a valid
installed tree before replacing the path with candidate.plugin_path.
wework/src/api/local/codexPlugins.ts (3)

470-488: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

warmLocalCodexPluginsReadState caches the resolved promise forever.

warmupReadStatePromise is cleared on failure but not after a successful resolution. A later call returns the original state object instead of a current snapshot. Only clearLocalCodexPluginsReadStateCache resets it. If warm-once is the intent, state that in the doc comment. Otherwise clear the memo after the promise settles and let readState deduplicate through inflightReadState.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/api/local/codexPlugins.ts` around lines 470 - 488, Update
warmLocalCodexPluginsReadState so warmupReadStatePromise is cleared after
successful settlement as well as failure, allowing later calls to obtain a
current snapshot while relying on readState’s inflightReadState deduplication.
Preserve the existing warmup logging and error propagation, and ensure the cache
reset occurs only after the promise has settled.

1454-1485: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Passing installedResponse.marketplaces as both arguments hides intent.

mergeInstalledPluginSummaries(installed, available) receives the same list twice. The call reads like a copy-paste defect even though it is deliberate. Add a named local or a short comment that states there is no available catalog in this path.

♻️ Proposed fix
+  // No plugin/list here, so the installed response is also the only "available" source.
+  const installedMarketplaces = installedResponse.marketplaces
   const installedPlugins = applyPluginCloudLinks(
     preferWeworkPersonalInstalled(
-      mergeInstalledPluginSummaries(installedResponse.marketplaces, installedResponse.marketplaces)
+      mergeInstalledPluginSummaries(installedMarketplaces, installedMarketplaces)
     ),
     cloudLinks
   )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/api/local/codexPlugins.ts` around lines 1454 - 1485, Add a clearly
named local or concise comment near the mergeInstalledPluginSummaries call in
loadInstalledPluginsOnly explaining that this path has no separate available
plugin catalog and intentionally passes installedResponse.marketplaces for both
arguments. Preserve the existing behavior and merge inputs.

435-462: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

peekLocalCodexPluginsReadState mutates the shared cache, which makes the freshness check ambiguous.

The peek writes cachedState, cachedStateAt, and cachedStateParamsKey when it loads a persisted entry. isLocalCodexPluginsReadStateFresh then compares Date.now() against cachedStateAt, which the peek may have just rebound to a different paramsKey. listInstalledPlugins calls both a keyed peek and an unkeyed peek at Line 1726 and Line 1731, so the freshness result can describe a different snapshot than the returned one.

Return the entry timestamp from a single lookup instead of reading module state twice.

♻️ Proposed direction
+function lookupReadStateSnapshot(paramsKey: string): PersistedReadStateEntry | null {
+  if (cachedState && cachedStateParamsKey === paramsKey) {
+    return { paramsKey, cachedAt: cachedStateAt, state: cachedState }
+  }
+  const persisted = readPersistedReadStateStore().entries[paramsKey]
+  if (!persisted) return null
+  if (Date.now() - persisted.cachedAt > READ_STATE_DURABLE_TTL_MS) return null
+  return persisted
+}

Build both peekLocalCodexPluginsReadState and isLocalCodexPluginsReadStateFresh on that helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/api/local/codexPlugins.ts` around lines 435 - 462, Introduce a
shared non-mutating lookup helper for the requested params that returns the
persisted state and its timestamp after hydration and TTL validation. Update
peekLocalCodexPluginsReadState and isLocalCodexPluginsReadStateFresh to use this
helper, with freshness calculated from the same lookup result rather than
cachedStateAt; preserve existing null and boolean behavior without rebinding the
shared cache during a peek.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/tests/services/test_plugin_marketplace_v2.py`:
- Around line 934-990: Add type annotations to
test_list_plugins_batches_grant_lookups_instead_of_per_plugin_queries, including
its test_db and test_user fixture parameters, and to before_cursor_execute’s
connection, cursor, statement, parameters, context, executemany, and return
type. Use the repository’s existing SQLAlchemy and test fixture annotation
conventions without changing test behavior.

In `@wework/src-tauri/src/local_executor.rs`:
- Around line 3877-3886: Update the marketplace manifest handling in the
disk-listing flow around marketplace_plugin_names: treat failures from reading
or parsing the manifest as an empty marketplace result instead of propagating
the error with ?. Preserve the existing plugin insertion for valid manifests and
allow installed-tree and Codex cache scans to continue when the manifest is
invalid.

In `@wework/src/api/local/codexPlugins.readStateCache.test.ts`:
- Around line 312-320: Add teardown for the Date.now mock used in the
stale-cache test around api.readState, preferably with afterEach(() =>
vi.restoreAllMocks()) so the advanced clock is restored after every test. Ensure
later tests do not inherit the mocked timestamp.

In `@wework/src/api/local/codexPlugins.ts`:
- Around line 1334-1343: Update the background revalidation cleanup in the
cached-state branch around loadReadStateSnapshot so the promise returned by
finally has a rejection handler that logs failures instead of becoming
unhandled. Preserve the existing inflightReadState deletion guard, while leaving
direct awaiting callers of existingInflight able to receive the original
rejection.

In `@wework/src/components/plugins/PluginsWorkspace.test.tsx`:
- Around line 3018-3019: Update the asynchronous assertions in the
pendingLocalList resolution scenarios around the existing resolveLocalList
calls, including both the 3018/3019 case and the corresponding 3054-3060 case.
Wrap each post-resolution plugin strip assertion in waitFor so the promise
callbacks and React re-render complete before verifying the strip remains
present.

In `@wework/src/components/plugins/PluginsWorkspace.tsx`:
- Around line 2436-2458: Update the personal disk-row seeding logic around
toMarketplaceOptions and setSelectedMarketplaceKey so the selected key is chosen
only from the generated cloud-available marketplace options. Do not select
cloudMarketplaceKey() unless that key exists in seeded (or the corresponding
available market list); otherwise fall back to seeded[0]?.key, preserving the
existing current selection when valid.

In `@wework/src/features/workbench/WorkbenchProvider.tsx`:
- Around line 1748-1769: Update the composer plugin loading flow around
loadComposerPluginApps and readLocalInstalledPlugins so currentComposerDeviceId
is resolved from cached Codex state or ensureLocalExecutorStarted before calling
localPluginApi.listInstalledPlugins(). Keep the installed-plugin response
handling unchanged, and ensure the resolved device ID is available to cloud
plugin loading even when listInstalledPlugins() rejects.

---

Nitpick comments:
In `@backend/tests/services/test_plugin_marketplace_v2.py`:
- Around line 974-1010: Extend the test setup around
PluginMarketplaceService.list_plugins to create an approved namespace membership
and a corresponding namespace plugin grant, then assert the expected plugin is
returned. Update the query-count assertion to cover the namespace-name
resolution path and verify it remains batched rather than issuing per-plugin
lookups.

In `@wework/src-tauri/src/local_executor.rs`:
- Around line 3844-3867: The latest_cached_personal_plugin_root function should
select the highest semantic version from valid cache directory names instead of
prioritizing modification time. Parse each directory name as a version, ignore
unparseable names, and use modified time only when versions tie; preserve
manifest validation and return the selected directory path.
- Around line 3831-3841: The installed-tree preference check in the candidate
merge logic must recognize both manifest locations. Update the condition
guarding existing.plugin_path so it treats either .codex-plugin/plugin.json or
.claude-plugin/plugin.json as a valid installed tree before replacing the path
with candidate.plugin_path.

In `@wework/src/api/local/codexPlugins.ts`:
- Around line 470-488: Update warmLocalCodexPluginsReadState so
warmupReadStatePromise is cleared after successful settlement as well as
failure, allowing later calls to obtain a current snapshot while relying on
readState’s inflightReadState deduplication. Preserve the existing warmup
logging and error propagation, and ensure the cache reset occurs only after the
promise has settled.
- Around line 1454-1485: Add a clearly named local or concise comment near the
mergeInstalledPluginSummaries call in loadInstalledPluginsOnly explaining that
this path has no separate available plugin catalog and intentionally passes
installedResponse.marketplaces for both arguments. Preserve the existing
behavior and merge inputs.
- Around line 435-462: Introduce a shared non-mutating lookup helper for the
requested params that returns the persisted state and its timestamp after
hydration and TTL validation. Update peekLocalCodexPluginsReadState and
isLocalCodexPluginsReadStateFresh to use this helper, with freshness calculated
from the same lookup result rather than cachedStateAt; preserve existing null
and boolean behavior without rebinding the shared cache during a peek.

In `@wework/src/api/local/listPersonalMarketplaceFromDisk.test.ts`:
- Around line 26-105: Add a failure-path test to the
listPersonalMarketplacePluginsFromDisk suite that configures
mocks.invoke.mockRejectedValue(...), calls
listPersonalMarketplacePluginsFromDisk, and asserts it returns an empty array.
Keep the existing setup and success-path assertions unchanged.

In `@wework/src/components/plugins/PluginsWorkspace.test.tsx`:
- Around line 2489-2528: Use the same fetch-wrapper pattern in both tests:
update the local fetch mock around the test beginning “paints local Codex
plugins…” and the corresponding wrapper near the later test to obtain and call
fetch’s existing mock implementation via
vi.mocked(fetch).getMockImplementation(). Preserve marketplace request
interception while ensuring all delegated requests continue through the mock so
vi.mocked(fetch).mock.calls records them consistently.

In `@wework/src/components/plugins/PluginsWorkspace.tsx`:
- Around line 2603-2626: In the Promise.allSettled callback, simplify the
cloudResult guard by removing the redundant cloudItems.length check and return
directly when cloudResult.status is 'rejected'. Preserve the existing
fulfilled-result handling and partial catalog painting flow.
- Around line 2396-2511: Extract the shared catalog-merging pipeline into a
focused buildCatalogSnapshot({ cloudItems, cloudInstalled, localState,
diskPersonalItems }) helper outside the component. Move cache fallback
resolution, mergeDiskPersonalIntoLocalRows, mergeInstalledPlugins, and
mergeMarketplaceCatalog into that helper, while preserving each caller’s
existing previous-installed source and inputs. Replace the duplicated logic in
applyCatalogSnapshot, paintPartialCatalog, and the post-settlement disk branch
with calls to the helper.
- Around line 2339-2345: Move the declaration of diskPersonalItemsForMerge and
the other three merge accumulators above applyCatalogSnapshot, preserving their
existing let declarations and initialization. Ensure applyCatalogSnapshot can
safely reference all four variables without relying on asynchronous call sites.

In `@wework/src/features/plugins/loadComposerPluginApps.ts`:
- Around line 77-85: Remove the redundant pluginKeys mapping from the diagnostic
payload in loadComposerPluginApps, since pluginStates already provides the
identical key values. Preserve pluginStates and its existing key, enabled, and
installState fields unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 11cdc577-9139-407d-8c92-f02c146ba7aa

📥 Commits

Reviewing files that changed from the base of the PR and between 87598bb and 490c005.

📒 Files selected for processing (22)
  • backend/app/services/plugin_marketplace_service.py
  • backend/tests/services/test_plugin_marketplace_v2.py
  • wework/src-tauri/src/lib.rs
  • wework/src-tauri/src/local_executor.rs
  • wework/src/api/local/codexPlugins.readStateCache.test.ts
  • wework/src/api/local/codexPlugins.ts
  • wework/src/api/local/listPersonalMarketplaceFromDisk.test.ts
  • wework/src/components/chat/composer/ComposerToolbar.test.tsx
  • wework/src/components/chat/composer/PluginPickerMenu.tsx
  • wework/src/components/layout/DesktopWorkbenchMain.tsx
  • wework/src/components/plugins/PluginsWorkspace.test.tsx
  • wework/src/components/plugins/PluginsWorkspace.tsx
  • wework/src/components/plugins/findMarketplaceItemForInstalled.test.ts
  • wework/src/components/plugins/findMarketplaceItemForInstalled.ts
  • wework/src/components/plugins/marketplaceCatalogMerge.test.ts
  • wework/src/components/plugins/marketplaceCatalogMerge.ts
  • wework/src/features/plugins/loadComposerPluginApps.ts
  • wework/src/features/plugins/marketplaceIdentity.test.ts
  • wework/src/features/plugins/marketplaceIdentity.ts
  • wework/src/features/plugins/marketplaceSearch.test.ts
  • wework/src/features/plugins/marketplaceSearch.ts
  • wework/src/features/workbench/WorkbenchProvider.tsx
💤 Files with no reviewable changes (1)
  • wework/src/components/layout/DesktopWorkbenchMain.tsx

Comment thread wework/src-tauri/src/local_executor.rs Resolved
Comment thread wework/src/api/local/codexPlugins.ts Resolved
Hold back strip/actions while install+local-auth is in flight, keep catalog
mounted during post-install refresh, and address CodeRabbit findings on
background refresh, disk listing, and device-id resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.ts`:
- Around line 20-44: Scope blocked plugin identities by marketplace and plugin
key in holdBackInFlightMarketplaceInstalls, matching installed plugins via
raw.spec.source.marketplace or providerKey plus pluginKey; pass local-auth
identities in this scoped form rather than bare names. In
wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.ts lines
20-44, update blocked tracking and matching so same keys in other marketplaces
remain visible. In
wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.test.ts lines
92-116, add install and local-auth cases with duplicate keys across marketplaces
and assert only the pending marketplace row and installed-strip item are hidden.

In `@wework/src/features/workbench/WorkbenchProvider.tsx`:
- Around line 1767-1775: Update the catch around
localPluginApi.listInstalledPlugins() to log a focused diagnostic without
request-sensitive data, then preserve the last-known installed-plugin snapshot
instead of returning []. Keep successful responses, including an explicitly
empty response, unchanged so the caller can clear state only after confirmed
success.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 88573103-6888-4882-8508-a15688afd03c

📥 Commits

Reviewing files that changed from the base of the PR and between 490c005 and 6cd9b04.

📒 Files selected for processing (10)
  • backend/tests/services/test_plugin_marketplace_v2.py
  • wework/e2e/desktop/task-flow.e2e.mjs
  • wework/src-tauri/src/local_executor.rs
  • wework/src/api/local/codexPlugins.readStateCache.test.ts
  • wework/src/api/local/codexPlugins.ts
  • wework/src/components/plugins/PluginsWorkspace.test.tsx
  • wework/src/components/plugins/PluginsWorkspace.tsx
  • wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.test.ts
  • wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.ts
  • wework/src/features/workbench/WorkbenchProvider.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • backend/tests/services/test_plugin_marketplace_v2.py
  • wework/src/api/local/codexPlugins.readStateCache.test.ts
  • wework/src-tauri/src/local_executor.rs
  • wework/src/api/local/codexPlugins.ts
  • wework/src/components/plugins/PluginsWorkspace.test.tsx
  • wework/src/components/plugins/PluginsWorkspace.tsx

Comment on lines +20 to +44
const blockedNames = new Set<string>()
if (authKey) blockedNames.add(authKey)
const items = input.items.map(item => {
const blocked =
input.installingIds.has(item.id) ||
(authKey !== '' && item.name.trim().toLowerCase() === authKey)
if (!blocked) return item
blockedNames.add(item.name.trim().toLowerCase())
return {
...item,
installed: false,
installedLocally: false,
installedPluginId: null,
}
})

if (blockedNames.size === 0) {
return { items, installed: input.installed }
}

const installed = input.installed.filter(plugin => {
const pluginKey = (plugin.raw.spec.source.pluginKey || plugin.name).trim().toLowerCase()
const name = plugin.name.trim().toLowerCase()
return !blockedNames.has(pluginKey) && !blockedNames.has(name)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep the blocked-plugin identity marketplace-scoped.

blockedNames contains only item.name. Lines 27 and 43 can therefore hide a plugin from a different marketplace when both marketplaces use the same plugin key. This also makes authPluginKey block every matching card name.

  • wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.ts#L20-L44: Track the blocked identity with the marketplace and plugin key. Match installed rows with plugin.raw.spec.source.marketplace or providerKey plus pluginKey. Pass a scoped local-auth identity instead of a bare plugin name.
  • wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.test.ts#L92-L116: Add install and local-auth cases with equal plugin keys from two marketplaces. Assert that only the pending marketplace row and installed-strip item are hidden.
📍 Affects 2 files
  • wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.ts#L20-L44 (this comment)
  • wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.test.ts#L92-L116
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.ts` around
lines 20 - 44, Scope blocked plugin identities by marketplace and plugin key in
holdBackInFlightMarketplaceInstalls, matching installed plugins via
raw.spec.source.marketplace or providerKey plus pluginKey; pass local-auth
identities in this scoped form rather than bare names. In
wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.ts lines
20-44, update blocked tracking and matching so same keys in other marketplaces
remain visible. In
wework/src/components/plugins/holdBackInFlightMarketplaceInstalls.test.ts lines
92-116, add install and local-auth cases with duplicate keys across marketplaces
and assert only the pending marketplace row and installed-strip item are hidden.

Comment on lines +1767 to +1775
try {
const response = await localPluginApi.listInstalledPlugins()
currentComposerDeviceId =
peekLocalCodexPluginsReadState({ mergeAllMarketplaces: true })?.deviceId ||
peekLocalCodexPluginsReadState()?.deviceId ||
currentComposerDeviceId
return response.items
} catch {
return []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Preserve failure state instead of returning an empty installation set.

When listInstalledPlugins() rejects, this catch returns []. The caller cannot distinguish a transient local API failure from a confirmed empty installation set. On the current-generation allowEmptySnapshot path at Line 1840, this can call replaceComposerApps([]) and blank the composer picker. Preserve the last-known snapshot on failure, and clear it only after a successful empty response. Add focused logging without sensitive request data.

As per coding guidelines, do not hide primary-path state or synchronization bugs behind fallback behavior; add focused diagnostic logging before changing behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/features/workbench/WorkbenchProvider.tsx` around lines 1767 -
1775, Update the catch around localPluginApi.listInstalledPlugins() to log a
focused diagnostic without request-sensitive data, then preserve the last-known
installed-plugin snapshot instead of returning []. Keep successful responses,
including an explicitly empty response, unchanged so the caller can clear state
only after confirmed success.

Source: Coding guidelines

@qdaxb
qdaxb added this pull request to the merge queue Aug 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 10, 2026
@qdaxb
qdaxb added this pull request to the merge queue Aug 11, 2026
Merged via the queue into wecode-ai:main with commit 9c2ba5d Aug 11, 2026
50 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants