fix(wework): stabilize plugin marketplace cache, sync, and creation UX - #2584
fix(wework): stabilize plugin marketplace cache, sync, and creation UX#2584luckjun529-lang wants to merge 22 commits into
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
… flow Reuse package logos with name-initial fallbacks, persist marketplace cache without stripping icons by default, reuse the empty-task launcher for Plugin Creator, and clear cross-account cache leaks found in Code Review. Co-authored-by: Cursor <cursoragent@cursor.com>
Exact cache-key match only, so token clear cannot resurface another account's in-memory catalog under the anon key. Co-authored-by: Cursor <cursoragent@cursor.com>
… key Stop merging previous-account cloud rows or installs from React refs after a cache miss so logout and account switch cannot repaint the old catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
When the current marketplace cache key has a warm snapshot, do not fall back to installedPluginsRef after a switch — an empty cached install list is valid. Co-authored-by: Cursor <cursoragent@cursor.com>
Narrow pluginTrial metadata.name narrowing for tsc, and align the expanded composer plugin picker height assertion with the h-8 control. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR updates plugin marketplace metadata, asset resolution, caching, installation, composer integration, trial hydration, plugin task guidance, and Plugin Creator dismissal behavior. It also updates tests, localization, styling, and lifecycle coverage. ChangesPlugin marketplace and trial lifecycle
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
wework/src/components/plugins/PluginCreateWorkspace.tsx (1)
186-214: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftRestore project selection on mobile.
At Line 214,
showProjectWorkBar={!isMobile}removesproject-work-buttonon mobile. At Lines 189-197, the project-title action then only blurs the button. A selected project still renders as an interactive title inDesktopEmptyTaskLauncher, so mobile users cannot change the project.Use the shared project selector directly on mobile, or render a mobile-specific launcher that provides the same action. Add a mobile interaction test.
As per coding guidelines: “Split mobile and desktop components when layout or interaction differs materially; otherwise use responsive classes.”
🤖 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/PluginCreateWorkspace.tsx` around lines 186 - 214, Restore mobile project switching in PluginCreateWorkspace by ensuring the DesktopEmptyTaskLauncher project-title action opens the shared project selector when the project-work button is absent on mobile, rather than only blurring anchorElement. Preserve the existing desktop button-click behavior, and add a mobile interaction test verifying that selecting the project title opens the project selector.Source: Coding guidelines
🧹 Nitpick comments (12)
wework/src/components/chat/ChatInput.tsx (1)
238-537: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftSplit
PluginTrialTemplateStripinto focused components.This function now owns header rendering, task rows, AI refinement, errors, and footer controls. Extract the header, task list, and footer into focused components.
As per coding guidelines: “functions should remain focused, preferably under 50 lines.”
🤖 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/chat/ChatInput.tsx` around lines 238 - 537, Refactor PluginTrialTemplateStrip into focused components by extracting the header markup, task-list rendering (including renderTemplateRow and empty-state recommendation), and footer controls into separate components. Keep PluginTrialTemplateStrip responsible for state and composing these components, passing only the required props and callbacks while preserving existing behavior and test IDs.Source: Coding guidelines
wework/src/features/plugins/pluginMarketplaceCache.ts (1)
172-198: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider remembering the quota-stripped state for later writes.
persistSnapshotalways attempts the full-logo payload first. After the firstQuotaExceededError, every later marketplace refresh repeats the same failedsetItemwith a multi-megabyte string before retrying the slim payload. A module-level flag that is set on the first quota failure would skip the doomed attempt for the rest of the session.This is a cost optimization only; correctness is unaffected.
🤖 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/pluginMarketplaceCache.ts` around lines 172 - 198, Add a module-level flag tracking whether storage quota has been exceeded, and update persistSnapshot to choose the quota-stripped payload immediately once that flag is set. Set the flag when the full-logo write in persistSnapshot encounters a QuotaExceededError, while preserving the existing fallback and warning behavior.wework/src/features/plugins/pluginTrial.ts (1)
116-125: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueType the parameter with the shared resolved-logo type.
registerMentionIconFromResolveddeclareslogo: { url: string; source: string; contrastPad: boolean }. Every caller passes aResolvedPluginLogofromresolvePluginLogo, whosesourceis a'provided' | 'fallback'union. Widening tostringremoves the compile-time check on thelogo.source !== 'provided'comparison.Import and use
ResolvedPluginLogofrom@/components/plugins/plugin-assetsinstead.As per coding guidelines,
**/*.{ts,tsx}requires strict TypeScript.🤖 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/pluginTrial.ts` around lines 116 - 125, Update registerMentionIconFromResolved to import and use the shared ResolvedPluginLogo type from `@/components/plugins/plugin-assets` instead of the inline logo shape, preserving the existing source and URL checks.Source: Coding guidelines
wework/src/features/workbench/WorkbenchProvider.tsx (2)
1938-1968: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueRemove the redundant publish after connector synchronization.
Lines 1938-1940 call
publishComposerApps(apps). Line 1973-1974 publishes the sameappsvalue again, becauseappsis not reassigned between the two points. EachpublishComposerAppscall writes the snapshot and notifies every composer listener, so the picker re-renders twice for one load.Drop the earlier publish, or add a comment that explains why both are needed.
🤖 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 1938 - 1968, Remove the earlier publishComposerApps(apps) call in the connector synchronization flow, since the unchanged apps value is published again by the later enrichment path. Preserve the connector loading and error handling while ensuring each load notifies composer listeners only once.
434-439: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the repeated scope-entry removal into one helper.
The same five-line "copy the record, delete the scope key, return the original when absent" block now appears six times across
setDraftInput,dismissTrialGuideForScope, andapplyQueuedPluginTrial, for three different state maps. Each new scoped map adds three more copies.Extract a single helper and reuse it at every site.
As per coding guidelines: "Before adding code, search for and reuse existing components, services, utilities, and patterns; extract shared logic instead of duplicating it."
♻️ Proposed helper
+function clearScopeEntry<T>( + setState: Dispatch<SetStateAction<Record<string, T>>>, + scopeKey: string +): void { + setState(current => { + if (!current[scopeKey]) return current + const next = { ...current } + delete next[scopeKey] + return next + }) +}Then replace each block, for example:
- setTrialPluginAppByScope(current => { - if (!current[projectChatScopeKey]) return current - const next = { ...current } - delete next[projectChatScopeKey] - return next - }) + clearScopeEntry(setTrialPluginAppByScope, projectChatScopeKey)Also applies to: 475-480, 501-510, 525-530
🤖 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 434 - 439, Extract the repeated scope-entry removal logic into a shared helper in WorkbenchProvider.tsx, preserving the behavior of returning the original map when the scope key is absent and otherwise cloning then deleting it. Replace all six duplicated blocks across setDraftInput, dismissTrialGuideForScope, and applyQueuedPluginTrial for each affected state map with this helper.Source: Coding guidelines
wework/src/features/plugins/composerPluginMetadata.ts (1)
48-89: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAlign the implementation with the documented intent, or update the comment.
The comment states that marketplace logos are preferred "when the installed-plugin row only has unresolved relative assets". The code does not inspect
app.logoUrl; it replaces the logo whenever a marketplace match resolves a provided light logo. The result is the same package logo in the common case, so behavior is acceptable, but the comment is misleading for future maintainers.Also note the second lookup matches on
displayNameacross the whole catalog. Two catalog rows that share a display name resolve to the first match.🤖 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/composerPluginMetadata.ts` around lines 48 - 89, Update overlayMarketplaceLogosOnComposerApps to align its behavior with the documented intent: inspect app.logoUrl and only apply marketplace logos when the installed app has unresolved relative assets, preserving existing usable logos. Also avoid ambiguous displayName-only catalog matching by requiring an unambiguous marketplace match or otherwise preserving the app when multiple entries share that display name.wework/src/features/plugins/pluginMarketplaceCache.test.ts (1)
73-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for the quota-stripping and TTL branches.
This test proves the happy path where full data-URL logos persist. Two branches in
persistSnapshotandisUsableSnapshotstay untested:
- The
QuotaExceededErrorretry that writeslogosStripped: truewith nulled data-URL logos.- The seven-day TTL rejection in
isUsableSnapshot.Both branches decide what the user sees after restart. Stub
window.localStorage.setItemto throw aDOMException('', 'QuotaExceededError')for the first branch, and write an entry with an oldfetchedAtfor the second.I can generate both tests if you want.
🤖 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/pluginMarketplaceCache.test.ts` around lines 73 - 101, Add tests alongside the existing persistence test to cover the quota-retry and TTL-expiration paths. Stub window.localStorage.setItem to throw a QuotaExceededError on the first persist attempt, then assert the saved snapshot has logosStripped: true and data-URL logos nulled; add an old-fetchedAt snapshot and assert isUsableSnapshot rejects it after the seven-day TTL.wework/src/features/plugins/loadComposerPluginApps.ts (1)
30-59: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winBound plugin detail reads and include marketplace identity
readInstalledPluginForTrialperforms aplugin/readrequest per plugin and can retryreadState({ refresh: true }). Limit concurrency inenrichLocalPluginsWithDetails. BuilddetailedByKeykeys from both marketplace and plugin key; plugin-key-only keys overwrite details for same-named plugins from different marketplaces.🤖 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 30 - 59, Update enrichLocalPluginsWithDetails to limit concurrency while reading plugin details, including any refresh retries performed by readInstalledPluginForTrial, using the repository’s established concurrency-limiting utility or pattern. Change detailedByKey and lookup key construction to include both marketplace identity and plugin key, with the same composite key used for source plugins and resolved details, so plugins sharing a name across marketplaces do not overwrite one another.wework/src/components/plugins/plugin-assets.test.ts (1)
99-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for installed-plugin asset resolution.
This PR adds
installedPluginRoot,resolveInstalledPluginAssetPath, andresolveInstalledPluginLogo/resolveInstalledPluginLogoUrl. These contain the highest-risk new logic: package-root discovery fromskills/agents/commandspaths, and..traversal rejection. The file has no test for them.Add tests that cover a relative logo resolved against a discovered package root, and a
..traversal input that resolves to the name initial.🤖 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/plugin-assets.test.ts` around lines 99 - 111, Add tests in plugin-assets.test.ts for resolveInstalledPluginLogo/resolveInstalledPluginLogoUrl using installedPluginRoot discovery from skills, agents, or commands paths: verify a relative logo resolves against the discovered package root, and verify a logo containing .. traversal is rejected and falls back to the name initial.wework/src/components/plugins/plugin-assets.ts (1)
146-180: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the duplicated logo-resolution body.
resolveInstalledPluginLogoUrlandresolveInstalledPluginLogobuild the identical option object.resolvePluginLogoUrlalready returnsresolvePluginLogo(options).url. Two copies can drift and produce different branding between the URL path and the full-metadata path.Delegate the URL variant to the object variant.
♻️ Proposed refactor
export function resolveInstalledPluginLogoUrl( plugin: InstalledPlugin, appearanceMode: ResolvedAppearanceMode = currentPluginLogoAppearanceMode(), interfaceOverride?: PluginLogoInterfaceFields | null ): string { - const interfaceData = interfaceOverride ?? plugin.spec.interface - return resolvePluginLogoUrl({ - pluginKey: plugin.spec.source.pluginKey, - logo: resolveInstalledPluginAssetPath(plugin, interfaceData?.logo) || interfaceData?.logo, - logoDark: - resolveInstalledPluginAssetPath(plugin, interfaceData?.logoDark) || interfaceData?.logoDark, - composerIcon: - resolveInstalledPluginAssetPath(plugin, interfaceData?.composerIcon) || - interfaceData?.composerIcon, - appearanceMode, - }) + return resolveInstalledPluginLogo(plugin, appearanceMode, interfaceOverride).url }🤖 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/plugin-assets.ts` around lines 146 - 180, Update resolveInstalledPluginLogoUrl to delegate to resolveInstalledPluginLogo with the same arguments and return its .url value, removing the duplicated option-building and asset-resolution logic while leaving resolveInstalledPluginLogo as the single implementation.wework/src/api/local/codexPlugins.ts (1)
845-910: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffConsider reusing one relative-asset sanitizer.
safeRelativePluginAssetPathandisRelativePluginAssetPathhere duplicateisRelativePluginAssetPathand the segment sanitizer insideresolveInstalledPluginAssetPathinwework/src/components/plugins/plugin-assets.ts. The two copies can drift; for example, the plugin-assets copy treatsfile://explicitly while this copy relies only on the scheme regex.Extract one shared helper (for example in
plugin-assets.ts) and import it in both places. As per coding guidelines: "Before adding code, search for and reuse existing components, services, utilities, and patterns; extract shared logic instead of duplicating it."🤖 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 845 - 910, Extract the relative plugin asset validation and sanitization currently split between safeRelativePluginAssetPath/isRelativePluginAssetPath and resolveInstalledPluginAssetPath into one shared helper in plugin-assets.ts. Reuse that helper from resolvePluginInterfaceAssets and the installed-asset resolver, preserving the existing file:// handling, traversal rejection, and relative-path behavior.Source: Coding guidelines
wework/src/components/plugins/PluginsWorkspace.test.tsx (1)
1248-1249: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThis assertion depends on synchronous filtering.
expect(await screen.findByText('Documents'))passes today becausevisibleMarketplaceItemsfilters onnormalizedQueryand not ondebouncedQuery. If the debounce is applied to filtering (see the comment onPluginsWorkspace.tsxLines 1228-1237), this assertion needs fake timers or a longer wait. Update this test together with that change.🤖 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 1248 - 1249, Update the search test around userEvent.type and the Documents assertion to account for debounced filtering: enable and advance fake timers, or otherwise wait for the debounce interval before asserting the result. Keep the assertion validating that “Documents” appears after the search query is processed.
🤖 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/e2e/desktop/checkpoints.mjs`:
- Line 20: Update the segment dispatch configuration for
plugin-marketplace-lifecycle: add a dedicated guard that runs
verifyMarketplacePluginLifecycle, remove that call from the plugin-lifecycle
branch, and include plugin-marketplace-lifecycle in the CI classifier and
relevant path-selection logic so direct segment execution runs the correct flow.
In `@wework/src-tauri/tauri.conf.json`:
- Line 41: Update the asset protocol configuration’s allow list to replace
“**/*” with explicit resource and application-data directories required by the
application. Do not rely on the captured window origin for path authorization,
and preserve access only to those necessary locations.
In `@wework/src/api/local/codexPlugins.ts`:
- Around line 1003-1021: Update the inferredConnectors construction in the Codex
plugin detail processing flow to include only required apps with an explicit
known connector slug from app.slug. Remove the app-name slugification and
fallback to detail.summary.name, so apps without a connector slug are omitted
while declaredConnectors behavior remains unchanged.
In `@wework/src/components/chat/ChatInput.tsx`:
- Around line 436-464: Update showPluginTrialGuide in pluginTrial.ts to dispatch
the guide event for valid plugins even when their available template list is
empty, removing only the early return that blocks this case. Preserve existing
validation for invalid plugins, allow ChatInput’s recommendation action to
render based on onRefinePrompt availability, and add an integration test
covering selection of a plugin with zero templates.
- Around line 308-344: Update the task-row button and the dismiss, expand, and
refine controls in the relevant ChatInput components to use mobile dimensions of
at least 44px by 44px, replacing the current min-h-10 and h-7 sizing. Preserve
the compact dimensions only through the mobile breakpoint, then reduce them at
the tablet breakpoint or above.
In `@wework/src/components/chat/composer/composerMentions.ts`:
- Around line 62-82: Update logoFromComposerAppInventory to strip the plugin:
and wegent: prefixes from appId before deriving pluginKey, and apply the same
prefix normalization when comparing item.id in the inventory lookup. Preserve
existing matching behavior and add coverage for app://plugin: and app://wegent:
mentions resolving their inventory logos.
In `@wework/src/components/chat/composer/ProjectChatComposer.tsx`:
- Around line 406-421: Update the onKeyDown handler in ProjectChatComposer so
Shift+Backspace is treated like the other modified shortcuts and returns false
without calling onDismissInputLeadingContext; only an unmodified Backspace at
the empty leading selection should dismiss the context.
In `@wework/src/components/plugins/PluginManagementRows.tsx`:
- Around line 68-72: Update the three installed-plugin logo call sites to
resolve relative package assets through the installed-plugin resolver before
falling back to marketplace interfaces. In
wework/src/components/plugins/PluginManagementRows.tsx lines 68-72,
wework/src/components/plugins/PluginsWorkspace.tsx lines 4168-4171, and
wework/src/components/plugins/PluginsWorkspace.tsx lines 4219-4223, apply this
behavior to the installed logo inputs; preferably extend
resolvePreferredPluginLogo to accept the optional InstalledPlugin and centralize
root resolution there.
In `@wework/src/components/plugins/PluginsWorkspace.tsx`:
- Around line 1228-1237: The search lists currently filter on normalizedQuery
while debouncedQuery only drives the updating indicator. Update
visibleMarketplaceItems and visibleInstalledPlugins to filter using
debouncedQuery, preserving normalizedQuery for the debounce effect and ensuring
the spinner remains aligned with when results update.
- Around line 2035-2055: Add a rejection handler to the promise chain in the
install-preparation flow around prepareMarketplaceInstallItem,
connectionNamesRequiredForInstall, and executeMarketplaceInstall. Surface
failures through the existing setPluginOperationNotice or
setPluginMarketplaceState mechanism, while preserving the finally cleanup of
preparingInstallPluginIdsRef.
In `@wework/src/styles/globals.css`:
- Line 710: Update the color declaration near the currentColor value in
globals.css to use the lowercase currentcolor spelling required by the
configured Stylelint rule, without changing the declaration’s behavior.
---
Outside diff comments:
In `@wework/src/components/plugins/PluginCreateWorkspace.tsx`:
- Around line 186-214: Restore mobile project switching in PluginCreateWorkspace
by ensuring the DesktopEmptyTaskLauncher project-title action opens the shared
project selector when the project-work button is absent on mobile, rather than
only blurring anchorElement. Preserve the existing desktop button-click
behavior, and add a mobile interaction test verifying that selecting the project
title opens the project selector.
---
Nitpick comments:
In `@wework/src/api/local/codexPlugins.ts`:
- Around line 845-910: Extract the relative plugin asset validation and
sanitization currently split between
safeRelativePluginAssetPath/isRelativePluginAssetPath and
resolveInstalledPluginAssetPath into one shared helper in plugin-assets.ts.
Reuse that helper from resolvePluginInterfaceAssets and the installed-asset
resolver, preserving the existing file:// handling, traversal rejection, and
relative-path behavior.
In `@wework/src/components/chat/ChatInput.tsx`:
- Around line 238-537: Refactor PluginTrialTemplateStrip into focused components
by extracting the header markup, task-list rendering (including
renderTemplateRow and empty-state recommendation), and footer controls into
separate components. Keep PluginTrialTemplateStrip responsible for state and
composing these components, passing only the required props and callbacks while
preserving existing behavior and test IDs.
In `@wework/src/components/plugins/plugin-assets.test.ts`:
- Around line 99-111: Add tests in plugin-assets.test.ts for
resolveInstalledPluginLogo/resolveInstalledPluginLogoUrl using
installedPluginRoot discovery from skills, agents, or commands paths: verify a
relative logo resolves against the discovered package root, and verify a logo
containing .. traversal is rejected and falls back to the name initial.
In `@wework/src/components/plugins/plugin-assets.ts`:
- Around line 146-180: Update resolveInstalledPluginLogoUrl to delegate to
resolveInstalledPluginLogo with the same arguments and return its .url value,
removing the duplicated option-building and asset-resolution logic while leaving
resolveInstalledPluginLogo as the single implementation.
In `@wework/src/components/plugins/PluginsWorkspace.test.tsx`:
- Around line 1248-1249: Update the search test around userEvent.type and the
Documents assertion to account for debounced filtering: enable and advance fake
timers, or otherwise wait for the debounce interval before asserting the result.
Keep the assertion validating that “Documents” appears after the search query is
processed.
In `@wework/src/features/plugins/composerPluginMetadata.ts`:
- Around line 48-89: Update overlayMarketplaceLogosOnComposerApps to align its
behavior with the documented intent: inspect app.logoUrl and only apply
marketplace logos when the installed app has unresolved relative assets,
preserving existing usable logos. Also avoid ambiguous displayName-only catalog
matching by requiring an unambiguous marketplace match or otherwise preserving
the app when multiple entries share that display name.
In `@wework/src/features/plugins/loadComposerPluginApps.ts`:
- Around line 30-59: Update enrichLocalPluginsWithDetails to limit concurrency
while reading plugin details, including any refresh retries performed by
readInstalledPluginForTrial, using the repository’s established
concurrency-limiting utility or pattern. Change detailedByKey and lookup key
construction to include both marketplace identity and plugin key, with the same
composite key used for source plugins and resolved details, so plugins sharing a
name across marketplaces do not overwrite one another.
In `@wework/src/features/plugins/pluginMarketplaceCache.test.ts`:
- Around line 73-101: Add tests alongside the existing persistence test to cover
the quota-retry and TTL-expiration paths. Stub window.localStorage.setItem to
throw a QuotaExceededError on the first persist attempt, then assert the saved
snapshot has logosStripped: true and data-URL logos nulled; add an old-fetchedAt
snapshot and assert isUsableSnapshot rejects it after the seven-day TTL.
In `@wework/src/features/plugins/pluginMarketplaceCache.ts`:
- Around line 172-198: Add a module-level flag tracking whether storage quota
has been exceeded, and update persistSnapshot to choose the quota-stripped
payload immediately once that flag is set. Set the flag when the full-logo write
in persistSnapshot encounters a QuotaExceededError, while preserving the
existing fallback and warning behavior.
In `@wework/src/features/plugins/pluginTrial.ts`:
- Around line 116-125: Update registerMentionIconFromResolved to import and use
the shared ResolvedPluginLogo type from `@/components/plugins/plugin-assets`
instead of the inline logo shape, preserving the existing source and URL checks.
In `@wework/src/features/workbench/WorkbenchProvider.tsx`:
- Around line 1938-1968: Remove the earlier publishComposerApps(apps) call in
the connector synchronization flow, since the unchanged apps value is published
again by the later enrichment path. Preserve the connector loading and error
handling while ensuring each load notifies composer listeners only once.
- Around line 434-439: Extract the repeated scope-entry removal logic into a
shared helper in WorkbenchProvider.tsx, preserving the behavior of returning the
original map when the scope key is absent and otherwise cloning then deleting
it. Replace all six duplicated blocks across setDraftInput,
dismissTrialGuideForScope, and applyQueuedPluginTrial for each affected state
map with this helper.
🪄 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: cfb8dbd9-f679-471a-9167-86ec808388ae
📒 Files selected for processing (51)
frontend/src/apis/tasks.tswework/e2e/desktop/checkpoints.mjswework/src-tauri/tauri.conf.jsonwework/src/api/local/codexPlugins.tswework/src/components/chat/ChatInput.test.tsxwework/src/components/chat/ChatInput.tsxwework/src/components/chat/MessageList.test.tsxwework/src/components/chat/MessageList.tsxwework/src/components/chat/composer/ComposerPluginIcon.tsxwework/src/components/chat/composer/ComposerTextarea.test.tsxwework/src/components/chat/composer/ComposerTextarea.tsxwework/src/components/chat/composer/PluginPickerMenu.tsxwework/src/components/chat/composer/ProjectChatComposer.tsxwework/src/components/chat/composer/SlashCommandMenu.tsxwework/src/components/chat/composer/composerMentions.test.tswework/src/components/chat/composer/composerMentions.tswework/src/components/layout/DesktopEmptyTaskLauncher.tsxwework/src/components/plugins/PluginCreateWorkspace.test.tsxwework/src/components/plugins/PluginCreateWorkspace.tsxwework/src/components/plugins/PluginDetailView.tsxwework/src/components/plugins/PluginManagementRows.test.tsxwework/src/components/plugins/PluginManagementRows.tsxwework/src/components/plugins/PluginSourceAvatar.tsxwework/src/components/plugins/PluginsWorkspace.test.tsxwework/src/components/plugins/PluginsWorkspace.tsxwework/src/components/plugins/findMarketplaceItemForInstalled.tswework/src/components/plugins/marketplaceCatalogMerge.test.tswework/src/components/plugins/plugin-assets.test.tswework/src/components/plugins/plugin-assets.tswework/src/components/plugins/plugin-dialogs/InstallPluginDialog.tsxwework/src/components/plugins/plugin-dialogs/PluginUseCaseGuideDialog.test.tsxwework/src/components/plugins/plugin-dialogs/PluginUseCaseGuideDialog.tsxwework/src/components/plugins/plugin-guide-presentation.test.tswework/src/components/plugins/plugin-guide-presentation.tswework/src/features/auth/AuthProvider.tsxwework/src/features/plugins/composerPluginMetadata.test.tswework/src/features/plugins/composerPluginMetadata.tswework/src/features/plugins/loadComposerPluginApps.test.tswework/src/features/plugins/loadComposerPluginApps.tswework/src/features/plugins/pluginMarketplaceCache.test.tswework/src/features/plugins/pluginMarketplaceCache.tswework/src/features/plugins/pluginTrial.test.tswework/src/features/plugins/pluginTrial.tswework/src/features/workbench/WorkbenchProvider.test.tsxwework/src/features/workbench/WorkbenchProvider.tsxwework/src/features/workbench/workbenchContextTypes.tswework/src/i18n/locales/en/common.jsonwework/src/i18n/locales/zh-CN/common.jsonwework/src/pages/PluginsPage.tsxwework/src/styles/globals.csswework/src/types/api.ts
💤 Files with no reviewable changes (5)
- wework/src/pages/PluginsPage.tsx
- wework/src/components/plugins/plugin-dialogs/PluginUseCaseGuideDialog.tsx
- wework/src/components/plugins/plugin-dialogs/PluginUseCaseGuideDialog.test.tsx
- wework/src/components/plugins/plugin-guide-presentation.test.ts
- wework/src/components/plugins/plugin-guide-presentation.ts
| ] | ||
|
|
||
| export const PLUGIN_SEGMENTS = [ | ||
| 'plugin-marketplace-lifecycle', |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 4 'plugin-marketplace-lifecycle|PLUGIN_SEGMENTS|DESKTOP_CHECKPOINTS' wework/e2e/desktop
fd -a -t f -g 'package.json' -g '*.yml' -g '*.yaml' . \
| xargs -r rg -n -C 3 'plugin-marketplace-lifecycle|desktop.*e2e|e2e.*desktop'Repository: wecode-ai/Wegent
Length of output: 10820
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- AGENTS guidance ---'
if [ -f wework/AGENTS.md ]; then
cat -n wework/AGENTS.md
fi
printf '%s\n' '--- desktop E2E files ---'
git ls-files 'wework/e2e/desktop' '.github' 'wework/package.json' \
| grep -E '(^wework/e2e/desktop/|^\.github/|wework/package\.json$)' \
| sed -n '1,240p'
printf '%s\n' '--- scenario and CI references ---'
rg -n -C 6 --glob '!node_modules/**' \
'plugin-marketplace-lifecycle|PLUGIN_SEGMENTS|run-checkpoints|e2e:desktop|plugins-only|desktop E2E|desktop.*checkpoint|checkpoint.*desktop' \
wework .github 2>/dev/null | sed -n '1,320p'Repository: wecode-ai/Wegent
Length of output: 45011
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- checkpoint runner ---'
cat -n wework/e2e/desktop/run-checkpoints.mjs | sed -n '1,330p'
printf '%s\n' '--- desktop checkpoint and plugin flow references ---'
rg -n -C 8 \
'shouldRunPluginSegment|pluginMarketplace|marketplace|plugin-lifecycle|skill-mention-rendering|sites-plugin-auto-install|PLUGIN_SEGMENTS' \
wework/e2e/desktop | sed -n '1,420p'
printf '%s\n' '--- workflow matrices and classifier logic ---'
sed -n '240,330p' .github/workflows/wework-e2e.yml
sed -n '1,260p' .github/scripts/classify-wework-desktop-e2e.sh
sed -n '260,520p' .github/scripts/classify-wework-desktop-e2e.shRepository: wecode-ai/Wegent
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- task-flow plugin dispatch ---'
rg -n -C 12 \
'shouldRunPluginSegment|verifyMarketplacePluginLifecycle|verifyPlugin|plugin-flows|PLUGIN_SEGMENTS|plugin-marketplace-lifecycle' \
wework/e2e/desktop/task-flow.e2e.mjs \
wework/e2e/desktop/modules/*.mjs
printf '%s\n' '--- relevant task-flow sections ---'
sed -n '1,180p' wework/e2e/desktop/task-flow.e2e.mjs
sed -n '180,420p' wework/e2e/desktop/task-flow.e2e.mjs
printf '%s\n' '--- plugin CI matrix generation ---'
rg -n -C 10 \
'wework_desktop_other_e2e_matrix|e2e:desktop:plugins|plugins-plugin|PLUGIN|plugin' \
.github/scripts/classify-wework-desktop-e2e.sh \
.github/workflows/wework-e2e.yml | sed -n '1,260p'Repository: wecode-ai/Wegent
Length of output: 38423
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
checkpoints = Path("wework/e2e/desktop/checkpoints.mjs").read_text()
task_flow = Path("wework/e2e/desktop/modules/task-flow-main.mjs").read_text()
classifier = Path(".github/scripts/classify-wework-desktop-e2e.sh").read_text()
workflow = Path(".github/workflows/wework-e2e.yml").read_text()
segment = "plugin-marketplace-lifecycle"
plugin_array = re.search(
r"export const PLUGIN_SEGMENTS = \[(.*?)\]",
checkpoints,
re.S,
).group(1)
classifier_array = re.search(
r"plugin_segments=\((.*?)\)",
classifier,
re.S,
).group(1)
print("checkpoint_registered:", segment in plugin_array)
print("classifier_registered:", re.search(rf"\b{re.escape(segment)}\b", classifier_array) is not None)
print("dedicated_dispatch_guard:",
f"shouldRunPluginSegment('{segment}')" in task_flow)
print("marketplace_flow_guarded_by_plugin_lifecycle:",
bool(re.search(
r"shouldRunPluginSegment\('plugin-lifecycle'\).*?"
r"verifyMarketplacePluginLifecycle",
task_flow,
re.S,
)))
print("standard_plugin_command_in_workflow:",
"e2e:desktop:plugins" in workflow)
print("workflow_passes_segment_to_command:",
bool(re.search(
r"pnpm --filter wework .*E2E_COMMAND.*args",
workflow,
)))
PY
printf '%s\n' '--- exact dispatch and classifier ranges ---'
cat -n wework/e2e/desktop/modules/task-flow-main.mjs | sed -n '980,1045p'
cat -n .github/scripts/classify-wework-desktop-e2e.sh | sed -n '1,75p;250,272p'
cat -n .github/workflows/wework-e2e.yml | sed -n '390,402p'Repository: wecode-ai/Wegent
Length of output: 6966
Dispatch plugin-marketplace-lifecycle as an independent CI segment
PLUGIN_SEGMENTS lists the segment, but task-flow-main.mjs runs verifyMarketplacePluginLifecycle under plugin-lifecycle. A direct --segment plugin-marketplace-lifecycle therefore runs no plugin flow and still reports success. Add a dedicated guard, move this call out of plugin-lifecycle, and add the segment to the CI classifier and relevant path selection.
🤖 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/e2e/desktop/checkpoints.mjs` at line 20, Update the segment dispatch
configuration for plugin-marketplace-lifecycle: add a dedicated guard that runs
verifyMarketplacePluginLifecycle, remove that call from the plugin-lifecycle
branch, and include plugin-marketplace-lifecycle in the CI classifier and
relevant path-selection logic so direct segment execution runs the correct flow.
Source: Coding guidelines
| "allow": [ | ||
| "**/*" | ||
| ] | ||
| "allow": ["**/*"] |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🌐 Web query:
For the Tauri version declared by this repository, what filesystem access does assetProtocol.scope.allow: ["**/*"]grant whenrequireLiteralLeadingDot is false? Can untrusted WebView content use the asset protocol to read arbitrary local files? Cite official Tauri documentation.
💡 Result:
The Tauri asset protocol configuration assetProtocol.scope.allow: ["/"] grants the WebView permission to access all files on the filesystem [1][2][3]. When requireLiteralLeadingDot is set to false, this glob pattern also encompasses hidden files and directories (those starting with a dot) [1][2][4]. Yes, untrusted WebView content can use the asset protocol to read arbitrary local files if the scope is configured this broadly [5][6]. The asset protocol is a custom URI scheme that serves files from the local filesystem to the WebView [1][7]. If the security configuration (in tauri.conf.json) explicitly allows access to the entire filesystem via wildcard globs, any resource, including sensitive system files, configuration files, or credentials, can be loaded into the WebView if an attacker knows or can guess their path [5]. Official Tauri documentation emphasizes that the asset protocol scope must be defined carefully [1]. Because this configuration serves as a bypass to the typical browser-based origin security model by exposing host files to the WebView, granting broad access like ["/"] is strongly discouraged [1][5]. Tauri recommends restricting the scope to specific, required directories using variables such as $APPDATA or $HOME, and using more granular patterns to minimize the attack surface [1][8][5].
Citations:
- 1: https://v2.tauri.app/security/asset-protocol/
- 2: https://v2.tauri.app/reference/config/
- 3: [bug] convertFileSrc doesn't respect persisted scope plugin tauri-apps/tauri#13788
- 4: https://github.com/tauri-apps/tauri-docs/blob/v2/src/content/docs/plugin/file-system.mdx
- 5: [Security] Asset Protocol scope allows reading arbitrary files from filesystem cjpais/Handy#1384
- 6: How to play video file selected by user on webview? tauri-apps/tauri#3735
- 7: https://v2.tauri.app/reference/javascript/api/namespacecore/
- 8: https://takazudomodular.com/pj/zudo-tauri/docs/frontend/asset-protocol/
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- declared Tauri versions ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' '"`@tauri-apps/`(api|cli|plugin-[^"]+)"|tauri-build|tauri =|tauri-build' wework package.json '*lock*' 2>/dev/null || true
printf '%s\n' '--- configuration ---'
cat -n wework/src-tauri/tauri.conf.json | sed -n '1,90p'
printf '%s\n' '--- repository guidance ---'
if [ -f wework/AGENTS.md ]; then cat wework/AGENTS.md; fi
printf '%s\n' '--- asset protocol references ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'assetProtocol|requireLiteralLeadingDot|asset:' weworkRepository: wecode-ai/Wegent
Length of output: 17679
🌐 Web query:
site:v2.tauri.app/security/asset-protocol Tauri 2 assetProtocol scope allow requireLiteralLeadingDot **/* arbitrary files WebView
💡 Result:
In Tauri 2, the asset protocol scope configuration controls which files are accessible to the WebView via the asset protocol [1]. When using the object form of the scope configuration, the requireLiteralLeadingDot option determines how glob patterns interact with files and directories that begin with a dot (hidden files or directories) on Unix-like systems [1]. By default, on Unix systems, requireLiteralLeadingDot is set to true [1]. This means that wildcard tokens such as * and will not match a path component starting with a dot [1]. For example, a glob pattern like $HOME//* will not include files or directories within a hidden directory (e.g., $HOME/.config/file.txt) unless the pattern explicitly includes the dot or requireLiteralLeadingDot is disabled [1]. If you set requireLiteralLeadingDot to false, you allow glob patterns to match path components that start with a dot [1]. This effectively widens the scope, allowing the WebView to access hidden files and directories that were previously excluded by the wildcard behavior [1]. The Tauri documentation emphasizes that this change should be reviewed carefully, as it increases the surface area for file access within your application [1]. To use this configuration, you must use the object form of the scope, which allows you to define allow and deny lists alongside the requireLiteralLeadingDot setting [1]: { "app": { "security": { "assetProtocol": { "enable": true, "scope": { "requireLiteralLeadingDot": false, "allow": ["$HOME//*"] } } } } } In this example, setting requireLiteralLeadingDot to false enables access to hidden files and directories under the $HOME directory [1].
Citations:
🌐 Web query:
site:github.com/tauri-apps/tauri v2.11.5 asset protocol scope requireLiteralLeadingDot glob **/*
💡 Result:
In Tauri, particularly on Unix-based systems (Linux/macOS), the requireLiteralLeadingDot configuration option controls whether glob patterns in your asset or file system scope are required to explicitly match a literal dot (.) for files or directories starting with a dot [1][2]. When requireLiteralLeadingDot is set to true (which is the default on Unix systems [1]), glob patterns like /* will not automatically match hidden files or directories—those starting with a dot [1][3]. For example, /* would fail to match files inside a .cache folder [3]. To access these hidden files/directories, you have two primary options: 1. Configure requireLiteralLeadingDot to false in your application configuration to allow the glob pattern to match dotfiles [4][2]. 2. Adjust your glob pattern to explicitly include the dot (e.g., use /.cache//*) [3]. The requireLiteralLeadingDot option was introduced to address security concerns regarding the accidental exposure of hidden system or configuration files [1][5]. In Tauri v2, if you are configuring this for the file system (fs) or asset protocol, it is typically set within the plugin configuration section of your tauri.conf.json [2][6]. If you are using /* and finding that assets (such as images) are not loading, it is highly likely that they are located within a hidden directory, and you must either explicitly add that path to your scope or disable requireLiteralLeadingDot [3][7].
Citations:
- 1: https://www.github.com/tauri-apps/tauri/commit/acc36fe1176cc8aa9063bde932abeb94796c5c72
- 2: [bug] Enumerate hidden directories on linux tauri-apps/tauri#7190
- 3: [v2] Display an image using the asset protocol. tauri-apps/tauri#11498
- 4: [bug] scope issues with fs on Linux tauri-apps/tauri#8653
- 5: [bug]
assetScopesuddenly no longer working? tauri-apps/tauri#6158 - 6: tauri v2 uses https://asset.localhost net::ERR_CONNECTION_REFUSED[bug] tauri-apps/tauri#9359
- 7: assetProtocol is not documented enough / missing requireLiteralLeadingDot example tauri-apps/tauri-docs#3791
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from urllib.request import urlopen
urls = [
"https://v2.tauri.app/security/asset-protocol/",
"https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v2.11.5/crates/tauri/src/protocol/asset.rs",
"https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v2.11.5/crates/tauri-utils/src/config.rs",
]
for url in urls:
print(f"\n--- {url} ---")
try:
text = urlopen(url, timeout=15).read().decode()
for i, line in enumerate(text.splitlines(), 1):
low = line.lower()
if any(term in low for term in (
"scope", "requireliteralleadingdot", "asset://", "localhost",
"origin", "allow", "deny", "glob",
)):
print(f"{i}: {line}")
except Exception as exc:
print(f"FETCH_ERROR: {exc}")
PYRepository: wecode-ai/Wegent
Length of output: 869
🌐 Web query:
site:v2.tauri.app/security/asset-protocol "asset protocol" "scope" "filesystem"
💡 Result:
The asset protocol in the Tauri framework is a custom URI scheme used to serve files from the local filesystem to a WebView [1]. For security reasons, the protocol is not open by default; it is governed by a scope configuration that explicitly defines which filesystem paths are accessible [1]. Configuration details: - Definition: The scope is defined in the tauri.conf.json file under the app.security.assetProtocol configuration object [1]. - Structure: The scope can be defined as an array of glob patterns or as a structured object containing allow, deny, and requireLiteralLeadingDot properties [1]. - Security Model: The asset protocol utilizes the same FsScope type as other filesystem operations in Tauri [1]. Paths requested by the frontend must match the defined allowed patterns, otherwise, the WebView will block the request with an error, typically stating that the asset protocol is not configured to allow the path [1]. - Glob Patterns: Tauri supports standard glob patterns such as *,?,, and [...] [1]. By default, these patterns do not match path components starting with a dot (e.g., hidden files or directories). To include these, users can set requireLiteralLeadingDot to false [1]. - Best Practices: Because resolved paths are often absolute, it is recommended to use absolute paths or supported environment variables (e.g., $HOME, $APPCONFIG) in the scope definition to ensure they align with the paths resolved at runtime [1].
Citations:
🌐 Web query:
site:github.com/tauri-apps/tauri/blob/tauri-v2.11.5 asset_protocol scope allow asset.rs
💡 Result:
The file is here:
crates/tauri/src/protocol/asset.rs on tag tauri-v2.11.5 [1]
The asset protocol scope check is at approximately lines 751–756:
if !scope.is_allowed(&path) {
log::error!("asset protocol not configured to allow the path: {path}");
return resp.status(403).body(Vec::new().into()).map_err(Into::into);
}[1] (github.com)
Citations:
🏁 Script executed:
#!/bin/bash
set -eu
fetch() {
url="$1"
printf '\n--- %s ---\n' "$url"
curl -fsSLk "$url"
}
fetch "https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v2.11.5/crates/tauri/src/protocol/asset.rs" \
| rg -n -C 8 'is_allowed|asset protocol|origin|UriScheme|request|scope'
fetch "https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v2.11.5/crates/tauri-utils/src/config.rs" \
| rg -n -C 10 'AssetProtocol|require_literal_leading_dot|requireLiteralLeadingDot|FsScope|scope'
printf '\n--- official documentation excerpts ---\n'
curl -fsSLk "https://v2.tauri.app/security/asset-protocol/" \
| sed 's/<[^>]*>/ /g' \
| tr -s '[:space:]' ' ' \
| fold -w 180 \
| rg -i -C 2 'scope|filesystem|origin|webview|localhost|hidden|dot|allow|deny'Repository: wecode-ai/Wegent
Length of output: 12910
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
try:
import glob
print("Python stdlib glob is not used for the Tauri decision; source retrieval is required.")
except Exception as exc:
print(exc)
PY
printf '%s\n' '--- Tauri glob implementation references ---'
curl -fsSLk "https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v2.11.5/crates/tauri-utils/src/path.rs" \
| rg -n -C 8 'is_allowed|require_literal_leading_dot|glob|FsScope' || true
curl -fsSLk "https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v2.11.5/crates/tauri-utils/src/config.rs" \
| rg -n -C 8 'is_allowed|FsScope|Glob|glob' || trueRepository: wecode-ai/Wegent
Length of output: 9548
🏁 Script executed:
#!/bin/bash
set -eu
for path in \
crates/tauri/src/scope/fs.rs \
crates/tauri/src/scope/mod.rs \
crates/tauri/src/webview/webview.rs \
crates/tauri/src/protocol/asset.rs
do
url="https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v2.11.5/$path"
printf '\n--- %s ---\n' "$url"
curl -fsSLk "$url" | rg -n -C 12 \
'is_allowed|Glob|glob|require_literal_leading_dot|requireLiteralLeadingDot|protocol::asset|get\(|window_origin|origin|register_uri_scheme'
doneRepository: wecode-ai/Wegent
Length of output: 16010
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import json, urllib.request, ssl
ctx = ssl._create_unverified_context()
url = "https://api.github.com/repos/tauri-apps/tauri/git/trees/tauri-v2.11.5?recursive=1"
data = json.load(urllib.request.urlopen(url, context=ctx, timeout=20))
for item in data.get("tree", []):
p = item["path"]
if p.startswith(("crates/tauri/src/scope/", "crates/tauri/src/protocol/")) and p.endswith(".rs"):
print(p)
PYRepository: wecode-ai/Wegent
Length of output: 355
🌐 Web query:
site:v2.tauri.app/security/asset-protocol untrusted content asset:// localhost origin
💡 Result:
Tauri’s relevant guidance is:
- The default asset protocol uses
asset://localhostand can be allowed in CSP, e.g.default-src 'self' customprotocol: asset:andimg-src ... asset:. (v2.tauri.app) - Avoid loading untrusted files or remote scripts; untrusted content can introduce attack vectors. (v2.tauri.app)
- Tauri’s API is restricted by origin, capabilities, permissions, and scopes; unallowed origins cannot invoke commands. (v2.tauri.app)
- The localhost plugin serves assets from
http://localhost, but Tauri explicitly warns that it introduces considerable security risks. (v2.tauri.app)
Thus, asset://localhost is generally safer than exposing the app through a localhost HTTP server, but it is not a trust boundary for untrusted HTML/JavaScript. Treat any untrusted asset as potentially able to exploit frontend vulnerabilities, and keep CSP, capabilities, and filesystem scopes restrictive.
Citations:
- 1: https://v2.tauri.app/security/csp/?utm_source=openai
- 2: https://v2.tauri.app/security/csp/?utm_source=openai
- 3: https://v2.tauri.app/security/runtime-authority/?utm_source=openai
- 4: https://v2.tauri.app/plugin/localhost/?utm_source=openai
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- scope decision ---'
curl -fsSLk "https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v2.11.5/crates/tauri/src/scope/fs.rs" \
| sed -n '410,455p'
printf '%s\n' '--- asset protocol registration and origin handling ---'
for path in \
crates/tauri/src/protocol/mod.rs \
crates/tauri/src/protocol/asset.rs \
crates/tauri/src/manager/webview.rs \
crates/tauri/src/webview/webview.rs
do
printf '\n--- %s ---\n' "$path"
curl -fsSLk "https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v2.11.5/$path" \
| rg -n -C 10 'asset::get|protocol::asset|get_response|window_origin|uri_scheme|register_uri|asset://|origin'
doneRepository: wecode-ai/Wegent
Length of output: 1559
🏁 Script executed:
#!/bin/bash
set -eu
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/src"
cat >"$tmp/Cargo.toml" <<'EOF'
[package]
name = "glob_probe"
version = "0.0.0"
edition = "2021"
[dependencies]
glob = "0.3"
EOF
cat >"$tmp/src/main.rs" <<'EOF'
use glob::{MatchOptions, Pattern};
use std::path::Path;
fn main() {
let pattern = Pattern::new("**/*").unwrap();
let options = MatchOptions {
require_literal_separator: true,
require_literal_leading_dot: false,
..Default::default()
};
for path in [
"/etc/passwd",
"/Users/example/.config/app/token",
"etc/passwd",
"Users/example/.config/app/token",
"bundled-plugins/plugin/index.html",
] {
println!("{path}: {}", pattern.matches_path_with(Path::new(path), options));
}
}
EOF
if command -v cargo >/dev/null 2>&1; then
(cd "$tmp" && cargo run --quiet)
else
echo 'cargo unavailable; no runtime probe executed'
fiRepository: wecode-ai/Wegent
Length of output: 401
🌐 Web query:
site:v2.tauri.app "asset protocol" "untrusted" files scope WebView
💡 Result:
In the context of the Tauri framework, the asset protocol is a mechanism used to securely serve files from the local filesystem into the webview [1]. Because loading arbitrary files from the filesystem can introduce security risks, particularly when handling untrusted files, Tauri implements a strict scoping mechanism to control and limit what the webview can access [1]. Key aspects of managing the asset protocol and its security scope include: Configuration: The asset protocol is disabled by default [2]. To enable it, you must set "enable": true in the tauri.conf.json file under the app.security.assetProtocol configuration object [1][3]. Scope Definition: You must explicitly define a scope to determine which filesystem paths are accessible to the webview [1]. This is configured using an array of glob patterns [2]. Paths resolved at runtime must match this scope; otherwise, the webview will refuse to load the asset, typically returning an error such as "asset protocol not configured to allow the path" [1]. Scope Format: The scope is defined as a list of allowed patterns [2]. It supports variable expansion (e.g., $HOME, $APPDATA, $RESOURCE) to reference system base directories securely [2]. Configuration options allow for fine-grained control, including an 'allow' list, an optional 'deny' list (which takes precedence over allowed paths), and a setting for 'requireLiteralLeadingDot' to manage access to hidden files [1][2]. Content Security Policy (CSP): Simply enabling the protocol is insufficient for webview access; you must also update your Content Security Policy (CSP) in tauri.conf.json to explicitly permit the asset protocol [3]. For example, including asset: or http://asset.localhost in your img-src or default-src directives is necessary for the webview to accept assets loaded via this protocol [4][3]. Best Practices: To mitigate risks associated with untrusted files, always restrict the scope to the minimum necessary directories [1]. Avoid overly broad patterns like ["*/"] [1]. If your application needs to access specific user-provided files or dynamic paths, validate those paths on the Rust side before attempting to serve them, or ensure they fall strictly within the defined, hardened scope [1][5]. Accessing files via the asset protocol does not grant the frontend the ability to perform arbitrary filesystem operations; those are governed separately by the application's capability system and IPC layer [6][5].
Citations:
- 1: https://v2.tauri.app/security/asset-protocol/
- 2: https://v2.tauri.app/reference/config/
- 3: https://v2.tauri.app/reference/javascript/api/namespacecore/
- 4: https://v2.tauri.app/security/csp/
- 5: https://v2.tauri.app/security/
- 6: https://v2.tauri.app/security/capabilities/
🌐 Web query:
site:github.com/tauri-apps/tauri "Access-Control-Allow-Origin" "asset protocol" window_origin
💡 Result:
In the context of Tauri, the "asset" protocol and "window_origin" relate to how the framework handles Cross-Origin Resource Sharing (CORS) for custom URI schemes [1][2]. When Tauri applications use custom protocols like asset:// or tauri:// to load local resources, they must navigate webview-enforced CORS policies [3][4]. To allow the frontend to access these local assets, Tauri dynamically sets the Access-Control-Allow-Origin response header [2][5][4]. The implementation typically involves the following: 1. Identification: The application determines the origin of the webview window (window_origin), which is often something like tauri://localhost [5][6]. 2. Header Injection: When a request is made to an asset:// URL, the Tauri asset protocol handler intercepts the request [1][2]. Before responding with the file content, it explicitly attaches the Access-Control-Allow-Origin header, setting its value to the captured window_origin [1][2][5]. 3. CORS Compliance: By setting this header to match the window's origin, the browser/webview permits the frontend to read the response, effectively preventing "Access-Control-Allow-Origin" errors that would otherwise block access to local files [3][4]. This mechanism ensures that assets are accessible only to the trusted application window [1][2]. Developers generally do not need to manually configure this header for standard asset loading, as Tauri automates it; however, issues may arise if there is a mismatch between the window origin and the expected protocol, or due to underlying limitations in the platform-specific webview engine (such as older versions of WebKitGTK or WebView2) [3][4][6][7].
Citations:
- 1: https://github.com/tauri-apps/tauri/blob/4222dd11/crates/tauri/src/protocol/asset.rs
- 2: https://www.github.com/tauri-apps/tauri/commit/45330e38193d0b2a01aa926aec433acc6b8f6597
- 3: not allowed by Access-Control-Allow-Origin tauri-apps/tauri#3874
- 4: [bug] custom scheme protocol not allowed to load js script file in iframe tauri-apps/tauri#3543
- 5: https://github.com/tauri-apps/tauri/blob/cdf5276478f8c41cedff92fd730e6f85986387e1/crates/tauri/src/protocol/tauri.rs
- 6: Strange CORS behavior. tauri-apps/tauri#2327
- 7: [feat] CORS workaround tauri-apps/tauri#8339
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- all Tauri 2.11.5 asset registration references ---'
python3 - <<'PY'
import json, ssl, urllib.request
ctx = ssl._create_unverified_context()
tree = json.load(urllib.request.urlopen(
"https://api.github.com/repos/tauri-apps/tauri/git/trees/tauri-v2.11.5?recursive=1",
context=ctx, timeout=20
))["tree"]
for item in tree:
path = item["path"]
if path.endswith(".rs") and path.startswith("crates/tauri/src/"):
try:
data = urllib.request.urlopen(
f"https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v2.11.5/{path}",
context=ctx, timeout=20
).read().decode()
except Exception:
continue
if any(x in data for x in ("asset::get", "protocol::asset", "window_origin", "asset_protocol")):
print(f"\n--- {path} ---")
lines = data.splitlines()
for i, line in enumerate(lines, 1):
if any(x in line for x in ("asset::get", "protocol::asset", "window_origin", "asset_protocol")):
lo, hi = max(1, i-8), min(len(lines), i+12)
for n in range(lo, hi+1):
print(f"{n}: {lines[n-1]}")
PYRepository: wecode-ai/Wegent
Length of output: 20751
Restrict the asset protocol scope to required resources.
In Tauri 2.11.5, ["**/*"] permits asset requests for arbitrary existing paths, including hidden files because requireLiteralLeadingDot is false. The captured window origin only sets CORS headers; it is not a path authorization boundary. Replace the wildcard with explicit resource and application-data directories.
🤖 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/tauri.conf.json` at line 41, Update the asset protocol
configuration’s allow list to replace “**/*” with explicit resource and
application-data directories required by the application. Do not rely on the
captured window origin for path authorization, and preserve access only to those
necessary locations.
| const declaredConnectors = detail.connectors ?? [] | ||
| const inferredConnectors: CodexPluginConnector[] = | ||
| declaredConnectors.length === 0 && | ||
| detail.summary.authPolicy?.trim().toLowerCase() === 'on_install' | ||
| ? (detail.apps ?? []) | ||
| .filter(app => app.required !== false) | ||
| .map(app => ({ | ||
| slug: | ||
| app.slug?.trim() || | ||
| app.name | ||
| .trim() | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9]+/g, '-') | ||
| .replace(/^-+|-+$/g, '') || | ||
| detail.summary.name, | ||
| authPolicy: 'on_install' as const, | ||
| })) | ||
| : [] | ||
| components.connectors = [...declaredConnectors, ...inferredConnectors].map(connector => { |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect connector inference consumers and connector slug sources.
set -euo pipefail
fd -t f 'codexPlugins.ts' wework/src | while IFS= read -r file; do
rg -n -C 6 'inferredConnectors|declaredConnectors|authPolicy' "$file"
done
rg -n -C 8 'ensureMarketplaceConnectors|plugins_connector_unavailable' --glob '*.tsx' --glob '*.ts' wework/src
rg -n -C 4 'isLocalConnector' wework/srcRepository: wecode-ai/Wegent
Length of output: 21474
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- isLocalConnector implementation ---'
sed -n '1,115p' wework/src/api/local/localConnectorAuth.ts
printf '%s\n' '--- connector app API and types ---'
rg -n -C 8 'listWegentConnectorApps|WegentConnectorApp|connector apps|connectorApps' wework/src
printf '%s\n' '--- app and connector payload sources ---'
rg -n -C 6 'required:|apps:|connectors:' wework/src --glob '*.{ts,tsx,json}' | head -n 500
printf '%s\n' '--- marketplace item construction and local install path ---'
sed -n '920,1065p' wework/src/api/local/codexPlugins.ts
sed -n '2035,2090p' wework/src/components/plugins/PluginsWorkspace.tsx
sed -n '2240,2290p' wework/src/components/plugins/PluginsWorkspace.tsxRepository: wecode-ai/Wegent
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- local plugin and connector schemas ---'
rg -n -C 5 'interface CodexPlugin(App|Summary|Detail)|type CodexPlugin(App|Summary|Detail)|interface CodexPluginConnector|connectors\??:|apps\??:' wework/src/api/local/codexPlugins.ts wework/src/types --glob '*.{ts,tsx}'
printf '%s\n' '--- auth policy and connector declarations ---'
rg -n -C 4 "'on_install'|\"on_install\"|authPolicy|auth_policy|localAuth|local_auth" wework/src wework --glob '*.{ts,tsx,json,md,yaml,yml}' --glob '!**/*.map' | head -n 500
printf '%s\n' '--- manifest read and marketplace conversion ---'
sed -n '1120,1320p' wework/src/api/local/codexPlugins.ts
sed -n '1360,1510p' wework/src/api/local/codexPlugins.ts
printf '%s\n' '--- focused connector-related workspace tests ---'
rg -n -C 10 'connector|on_install|unavailable|localMarketplace|installAvailablePlugin' wework/src/components/plugins/PluginsWorkspace.test.tsx | head -n 500Repository: wecode-ai/Wegent
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- exact detail-to-marketplace conversion ---'
sed -n '840,1055p' wework/src/api/local/codexPlugins.ts
printf '%s\n' '--- local marketplace test payload helpers ---'
sed -n '500,660p' wework/src/components/plugins/PluginsWorkspace.test.tsx
sed -n '1,180p' wework/src/api/local/codexPlugins.test.ts 2>/dev/null || true
printf '%s\n' '--- tracked plugin manifests and catalog data ---'
git ls-files | rg '(^|/)(plugin|marketplace|manifest|catalog|app|connector)' | head -n 300
rg -n -C 4 '"apps"|"connectors"|"authPolicy"|"auth_policy"' --glob '*.json' --glob '*.yaml' --glob '*.yml' --glob '*.toml' . | head -n 400
printf '%s\n' '--- deterministic inference/install probe ---'
node - <<'JS'
function infer(summary, detail) {
const declared = detail.connectors ?? []
return declared.length === 0 && summary.authPolicy?.trim().toLowerCase() === 'on_install'
? (detail.apps ?? [])
.filter(app => app.required !== false)
.map(app => ({
slug:
app.slug?.trim() ||
app.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') ||
summary.name,
authPolicy: 'on_install',
}))
: []
}
function ensure(required, apps) {
for (const requirement of required) {
const app = apps.find(candidate => candidate.slug === requirement.slug)
if (!app) return 'plugins_connector_unavailable'
}
return 'install proceeds'
}
const cases = [
{
name: 'app name differs from cloud slug',
summary: { name: 'local-plugin', authPolicy: 'on_install' },
detail: { apps: [{ id: '1', name: 'Internal Docs', required: true }] },
cloud: [{ slug: 'docs' }],
},
{
name: 'explicit app slug matches cloud slug',
summary: { name: 'local-plugin', authPolicy: 'on_install' },
detail: { apps: [{ id: '1', name: 'Internal Docs', slug: 'docs', required: true }] },
cloud: [{ slug: 'docs' }],
},
{
name: 'optional app is excluded',
summary: { name: 'local-plugin', authPolicy: 'on_install' },
detail: { apps: [{ id: '1', name: 'Internal Docs', required: false }] },
cloud: [],
},
]
for (const item of cases) {
const inferred = infer(item.summary, item.detail)
console.log(item.name, JSON.stringify(inferred), ensure(inferred, item.cloud))
}
JSRepository: wecode-ai/Wegent
Length of output: 27884
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- deterministic inference/install probe ---'
node - <<'JS'
function infer(summary, detail) {
const declared = detail.connectors ?? []
return declared.length === 0 && summary.authPolicy?.trim().toLowerCase() === 'on_install'
? (detail.apps ?? [])
.filter(app => app.required !== false)
.map(app => ({
slug:
app.slug?.trim() ||
app.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') ||
summary.name,
authPolicy: 'on_install',
}))
: []
}
function ensure(required, apps) {
for (const requirement of required) {
if (!apps.find(candidate => candidate.slug === requirement.slug)) {
return 'plugins_connector_unavailable'
}
}
return 'install proceeds'
}
const cases = [
['arbitrary app name', { name: 'local-plugin', authPolicy: 'on_install' },
{ apps: [{ id: '1', name: 'Internal Docs', required: true }] }, [{ slug: 'docs' }]],
['explicit matching slug', { name: 'local-plugin', authPolicy: 'on_install' },
{ apps: [{ id: '1', name: 'Internal Docs', slug: 'docs', required: true }] }, [{ slug: 'docs' }]],
['optional app', { name: 'local-plugin', authPolicy: 'on_install' },
{ apps: [{ id: '1', name: 'Internal Docs', required: false }] }, []],
]
for (const [name, summary, detail, cloud] of cases) {
const inferred = infer(summary, detail)
console.log(name, JSON.stringify(inferred), ensure(inferred, cloud))
}
JS
printf '%s\n' '--- local install call order ---'
sed -n '2055,2082p' wework/src/components/plugins/PluginsWorkspace.tsx
sed -n '2243,2262p' wework/src/components/plugins/PluginsWorkspace.tsx
printf '%s\n' '--- connector and app identifier contracts ---'
rg -n -C 5 'connector-apps|class .*Connector|ConnectorApp|connector.*slug|apps.*slug|required.*bool' \
backend/app/api/endpoints backend/app/schemas backend/app/services wework/src/api/local wework/src/api/cloud \
--glob '*.py' --glob '*.ts' | head -n 500Repository: wecode-ai/Wegent
Length of output: 40225
Restrict inferred connectors to apps with a known connector slug
CodexPluginDetail.apps.slug is optional, and app names are not connector identifiers. The inference creates an on_install connector from every required app. A missing exact match in /connector-apps throws plugins_connector_unavailable before the local install starts.
Use only apps with a known Wegent connector slug, or omit inferred connectors without one.
🤖 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 1003 - 1021, Update the
inferredConnectors construction in the Codex plugin detail processing flow to
include only required apps with an explicit known connector slug from app.slug.
Remove the app-name slugification and fallback to detail.summary.name, so apps
without a connector slug are omitted while declaredConnectors behavior remains
unchanged.
| <button | ||
| key={template.path} | ||
| type="button" | ||
| data-testid={ | ||
| isPrimaryRecommendation | ||
| ? 'plugin-trial-recommendation-apply' | ||
| : 'plugin-trial-template-card' | ||
| } | ||
| className="group relative flex min-h-10 w-full items-center gap-2.5 border-b border-border/15 px-3 py-1.5 text-left transition-colors last:border-b-0 hover:bg-blue-500/[0.08] focus-visible:bg-blue-500/[0.08] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-blue-500" | ||
| onClick={() => onApplyTemplate?.(template)} | ||
| aria-label={t('workbench.plugin_trial_apply_task', '填入任务:{{task}}', { | ||
| task: displayTitle, | ||
| }).replace('{{task}}', displayTitle)} | ||
| > | ||
| <span | ||
| className="absolute inset-y-2 left-0 w-0.5 rounded-r-full bg-blue-500 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100" | ||
| aria-hidden="true" | ||
| /> | ||
| <TaskIcon | ||
| className="h-4 w-4 shrink-0 text-blue-600 dark:text-blue-300" | ||
| aria-hidden="true" | ||
| /> | ||
| {pluginName && ( | ||
| <span className="shrink-0 text-sm font-medium leading-5 text-blue-600 dark:text-blue-300"> | ||
| {pluginName} | ||
| </span> | ||
| )} | ||
| <strong | ||
| className="min-w-0 flex-1 truncate text-sm font-normal leading-5 text-text-primary" | ||
| data-testid={isPrimaryRecommendation ? 'plugin-trial-recommendation-title' : undefined} | ||
| > | ||
| {displayTitle} | ||
| </strong> | ||
| <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-border/30 bg-background text-text-secondary transition-colors group-hover:border-blue-500/60 group-hover:bg-blue-500/[0.1] group-hover:text-blue-600 group-focus-visible:border-blue-500/60 group-focus-visible:bg-blue-500/[0.1] group-focus-visible:text-blue-600"> | ||
| <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" /> | ||
| </span> | ||
| </button> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Meet the mobile control target size.
The task rows use min-h-10 and the dismiss, expand, and refine controls use h-7. These controls remain 40px or 28px high in the compact composer. Increase their mobile dimensions to at least 44px, then reduce them only at the tablet breakpoint or above.
As per coding guidelines: “Mobile controls must be at least 44px × 44px.”
Also applies to: 382-390, 489-530
🤖 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/chat/ChatInput.tsx` around lines 308 - 344, Update the
task-row button and the dismiss, expand, and refine controls in the relevant
ChatInput components to use mobile dimensions of at least 44px by 44px,
replacing the current min-h-10 and h-7 sizing. Preserve the compact dimensions
only through the mobile breakpoint, then reduce them at the tablet breakpoint or
above.
Source: Coding guidelines
| const normalizedQuery = normalizeMarketplaceSearchQuery(query) | ||
| const isMarketplaceSearchUpdating = | ||
| Boolean(normalizedQuery) && (normalizedQuery !== debouncedQuery || isMarketplaceRefreshing) | ||
|
|
||
| useEffect(() => { | ||
| const timeoutId = window.setTimeout(() => { | ||
| setDebouncedQuery(normalizedQuery) | ||
| }, 300) | ||
| return () => window.clearTimeout(timeoutId) | ||
| }, [normalizedQuery]) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
debouncedQuery does not debounce the search.
visibleMarketplaceItems (Line 3315) and visibleInstalledPlugins (Line 3350) both filter on normalizedQuery, not on debouncedQuery. The list therefore re-ranks on every keystroke. debouncedQuery is used only inside isMarketplaceSearchUpdating, so the spinner reports "updating" for 300 ms after results are already final.
Pick one behavior: either filter on debouncedQuery so the spinner is accurate, or remove the debounce state and effect and derive the spinner from isMarketplaceRefreshing only.
🤖 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 1228 - 1237,
The search lists currently filter on normalizedQuery while debouncedQuery only
drives the updating indicator. Update visibleMarketplaceItems and
visibleInstalledPlugins to filter using debouncedQuery, preserving
normalizedQuery for the debounce effect and ensuring the spinner remains aligned
with when results update.
| if (preparingInstallPluginIdsRef.current.has(item.id)) return | ||
| preparingInstallPluginIdsRef.current.add(item.id) | ||
| void prepareMarketplaceInstallItem(item) | ||
| .then(async preparedItem => { | ||
| if (promptAfterInstall) { | ||
| executeMarketplaceInstall({ | ||
| item: preparedItem, | ||
| requiredConnectionNames: [], | ||
| promptAfterInstall, | ||
| }) | ||
| return | ||
| } | ||
| setPendingInstall({ | ||
| item: preparedItem, | ||
| requiredConnectionNames: await connectionNamesRequiredForInstall(preparedItem), | ||
| }) | ||
| }) | ||
| .finally(() => { | ||
| preparingInstallPluginIdsRef.current.delete(item.id) | ||
| }) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Add a rejection handler to the install preparation chain.
The chain has .then(...) and .finally(...) but no .catch(...). If connectionNamesRequiredForInstall or executeMarketplaceInstall throws, the promise rejects unhandled. setPendingInstall never runs, so the install dialog never opens and the user receives no error. The preparingInstallPluginIdsRef entry is still cleared, so the button stays silently inert on the first click.
Handle the rejection and surface it through setPluginOperationNotice or setPluginMarketplaceState.
🛡️ Proposed fix
})
+ .catch((error: Error) => {
+ setPluginOperationNotice({
+ id: `install-error-${item.id}`,
+ kind: 'error',
+ message: error.message,
+ })
+ })
.finally(() => {
preparingInstallPluginIdsRef.current.delete(item.id)
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (preparingInstallPluginIdsRef.current.has(item.id)) return | |
| preparingInstallPluginIdsRef.current.add(item.id) | |
| void prepareMarketplaceInstallItem(item) | |
| .then(async preparedItem => { | |
| if (promptAfterInstall) { | |
| executeMarketplaceInstall({ | |
| item: preparedItem, | |
| requiredConnectionNames: [], | |
| promptAfterInstall, | |
| }) | |
| return | |
| } | |
| setPendingInstall({ | |
| item: preparedItem, | |
| requiredConnectionNames: await connectionNamesRequiredForInstall(preparedItem), | |
| }) | |
| }) | |
| .finally(() => { | |
| preparingInstallPluginIdsRef.current.delete(item.id) | |
| }) | |
| } | |
| if (preparingInstallPluginIdsRef.current.has(item.id)) return | |
| preparingInstallPluginIdsRef.current.add(item.id) | |
| void prepareMarketplaceInstallItem(item) | |
| .then(async preparedItem => { | |
| if (promptAfterInstall) { | |
| executeMarketplaceInstall({ | |
| item: preparedItem, | |
| requiredConnectionNames: [], | |
| promptAfterInstall, | |
| }) | |
| return | |
| } | |
| setPendingInstall({ | |
| item: preparedItem, | |
| requiredConnectionNames: await connectionNamesRequiredForInstall(preparedItem), | |
| }) | |
| }) | |
| .catch((error: Error) => { | |
| setPluginOperationNotice({ | |
| id: `install-error-${item.id}`, | |
| kind: 'error', | |
| message: error.message, | |
| }) | |
| }) | |
| .finally(() => { | |
| preparingInstallPluginIdsRef.current.delete(item.id) | |
| }) | |
| } |
🤖 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 2035 - 2055,
Add a rejection handler to the promise chain in the install-preparation flow
around prepareMarketplaceInstallItem, connectionNamesRequiredForInstall, and
executeMarketplaceInstall. Surface failures through the existing
setPluginOperationNotice or setPluginMarketplaceState mechanism, while
preserving the finally cleanup of preparingInstallPluginIdsRef.
| justify-content: center; | ||
| border-radius: 0.2em; | ||
| background: rgb(var(--color-focus) / 0.1); | ||
| color: currentColor; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Fix the Stylelint keyword casing error.
Line 710 uses currentColor. The configured Stylelint rule requires currentcolor, so this stylesheet fails linting.
Proposed fix
- color: currentColor;
+ color: currentcolor;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| color: currentColor; | |
| color: currentcolor; |
🧰 Tools
🪛 Stylelint (17.14.0)
[error] 710-710: Expected "currentColor" to be "currentcolor" (value-keyword-case)
(value-keyword-case)
🤖 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/styles/globals.css` at line 710, Update the color declaration near
the currentColor value in globals.css to use the lowercase currentcolor spelling
required by the configured Stylelint rule, without changing the declaration’s
behavior.
Source: Linters/SAST tools
Example cards now open the non-auto-send plugin trial flow instead of the removed use-case guide dialog; update the desktop Plugins E2E accordingly. Co-authored-by: Cursor <cursoragent@cursor.com>
Returning from the trial chat resets marketplace filters/tabs, so clear filters and reselect the fixture market before asserting the plugin row. Co-authored-by: Cursor <cursoragent@cursor.com>
Official plugin install infers on_install connectors and calls GET /api/connector-apps before plugin/install; add E2E stubs and close the composer picker left open after marketplace uninstall checks. Co-authored-by: Cursor <cursoragent@cursor.com>
Escape alone did not close the picker after marketplace uninstall checks; reuse closeComposerPluginPicker so the official-plugin segment can continue. Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve marketplace conflicts by keeping cache-first paint and account cache keys, while adopting main durable peek, official empty state, and category sections. Co-authored-by: Cursor <cursoragent@cursor.com>
Prevent stale peek SWR from dropping DISABLED_BY_ADMIN rows or wiping warmed connector localAuth, and re-check plan/admin locks before marketplace install. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Root cause
After desktop resume, transient or incomplete local plugin responses could replace a valid OpenAI catalog snapshot. Desired cloud releases were also treated as if they were already materialized locally, producing incorrect installed/update states and repeated synchronization attempts. Legacy managed plugin artifacts accumulated across older directory layouts, but removing them safely required reference-driven ownership and strict filesystem boundaries.
Impact
The plugin marketplace paints immediately after a warm restart, preserves accurate device state, adapts its category layout to catalog size, and removes only unreferenced Wegent-managed artifacts. Personal and OpenAI marketplaces remain isolated, legacy caches remain compatible, and corrupted or malicious symlink roots are rejected.
Validation