Skip to content

Make directory examples actionable#170

Open
friuns2 wants to merge 8 commits into
mainfrom
codex/directory-action-examples
Open

Make directory examples actionable#170
friuns2 wants to merge 8 commits into
mainfrom
codex/directory-action-examples

Conversation

@friuns2
Copy link
Copy Markdown
Owner

@friuns2 friuns2 commented May 13, 2026

Summary

  • Adds concrete example chips for plugin, app, and Composio directory cards.
  • Makes examples actionable by starting task-specific prompts instead of generic Try it prompts.
  • Keeps examples visible even when an integration is not installed or connected, with disabled state until usable.
  • Pins a casual-user popular ordering for plugin/app/Composio catalogs while preserving heuristic fallback sorting.
  • Preloads plugin, app, and Composio catalog data in the background so the Skills tab can reuse it.

Verification

  • pnpm run build:frontend

Notes

  • This PR intentionally reflects reset commit 80d26945 and excludes the later task-launcher/action-redesign commits.

Summary by CodeRabbit

  • New Features

    • Added example prompt chips for plugins, apps, and Composio connectors to guide user actions
    • Implemented popular item pinning to surface frequently-used tools
  • Performance

    • Added in-memory caching for directory catalogs with refresh controls
    • Introduced background preloading to improve catalog loading speed
  • Documentation

    • Added test scenarios for example-driven interface validation

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Walkthrough

This PR introduces a client-side catalog preloading system with caching, extends the try action payload to support per-example prompts, adds pinned popularity rankings for plugins and Composio connectors, generates example chip rows in catalog cards and modals, refactors component loaders to reuse in-flight promises and support forced refreshes, and invalidates caches on state mutations.

Changes

Directory Catalog Preloading and Example-Driven Try

Layer / File(s) Summary
Cache infrastructure for directory APIs
src/api/codexGateway.ts
Introduces DirectoryCacheOptions, cache maps for plugins/apps/Composio, defensive cloning helpers, and a centralized cache-clear function. Implements cached versions of listDirectoryPlugins, listDirectoryApps, getDirectoryComposioStatus, and listDirectoryComposioConnectors with in-flight promise reuse and optional force refresh. Cache is invalidated after plugin/app install/uninstall/enable/disable.
DirectoryTryItemPayload extension with tryKey
src/components/content/DirectoryHub.vue, src/App.vue
Extends DirectoryTryItemPayload type with optional tryKey?: string field. Updates getDirectoryTryItemKey() in App.vue to prefer payload.tryKey when present; otherwise computes key from prompt, kind, name, and skillPath for per-example deduplication.
Example chips data model and popularity pinning
src/components/content/DirectoryHub.vue, src/components/content/directoryHubUtils.ts
Introduces pinned ranking constants (POPULAR_PLUGIN_TOP_20, POPULAR_APP_TOP_20, POPULAR_COMPOSIO_TOP_20) and example generation rules (PLUGIN_EXAMPLE_RULES). Implements normalizePopularRankName() and pinnedPopularRank() helpers that boost popularity scores for pinned items. Adds pluginExampleChips(), appExampleChips(), and composioExampleChips() generators. Updates popularity score functions and sort comparators to prioritize pinned rankings.
Refactored loaders with force flag and in-flight caching
src/components/content/DirectoryHub.vue
Updates loadPlugins(force), loadApps(force), and loadComposio(append, force) to accept optional force flags, maintain module-level state for in-flight promises and cache keys, and reuse promises to prevent concurrent duplicate requests. Passes force flag through to underlying API functions.
Example chips in cards and detail modals
src/components/content/DirectoryHub.vue
Restructures plugin, app, and Composio card markup to render example prompt chip rows with per-example try handlers. Adds "Examples" sections to plugin and Composio detail modals. Removes one-click "Try it!/Start" buttons. Adds scoped CSS for example chip layout and styling (light and dark modes).
Example-aware try functions
src/components/content/DirectoryHub.vue
Implements tryApp(), tryPlugin(), and tryComposio() that accept optional example parameter, generate per-example prompts via buildExamplePrompt(), compute example-specific try keys, and emit try-item events with prompt and tryKey in the payload. Includes per-item eligibility checks via canTryApp().
Search filtering with example chips
src/components/content/DirectoryHub.vue
Integrates generated example chips into search text for plugins, apps, and Composio connectors. Also includes plugin defaultPrompt in searchable text. Examples appear in search results and filtering.
Refresh and cache invalidation logic
src/components/content/DirectoryHub.vue
Updates refreshActiveTab(forceReload) and manualRefreshActiveTab() to forward forceReload to loaders. Wires loadPlugins(true) after plugin install/uninstall/enable/disable. Triggers forced Composio reload after connection changes. Updates props.cwd watcher to force plugin reload when active tab is plugins.
Catalog preloading on app startup
src/App.vue
Adds imports for directory catalog functions. Implements preloadDirectoryCatalogs() that gathers plugins, apps, and eligible Composio connectors using Promise.allSettled(), swallowing errors so navigation is not blocked. Schedules preload to run shortly after mount via setTimeout.
Test documentation for example chips feature
tests.md
Adds manual regression test section specifying prerequisites, steps covering Popular sorting with hardcoded pinned lists, example chip rendering and click behavior across catalog types, preload cache reuse across route transitions, refresh/mutation cache invalidation, and light/dark theme verification.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant DH as DirectoryHub
  participant Loader as loadPlugins(force)
  participant GW as codexGateway cache layer
  participant API as Backend API

  User->>DH: navigate to Plugins tab
  DH->>Loader: loadPlugins(false)
  Loader->>GW: check pluginsLoadedKey vs props.cwd
  alt Same key & promise exists
    Loader-->>Loader: return cached promise
  else Key changed or force=true
    Loader->>GW: listDirectoryPlugins({force:false})
    GW->>GW: check internal cache
    alt Cached (not force)
      GW-->>Loader: return cloned cached data
    else Not cached or force=true
      GW->>API: fetch fresh plugins list
      API-->>GW: plugins data
      GW->>GW: update cache
      GW-->>Loader: return cloned data
    end
    Loader->>Loader: store promise for key
  end
  Loader-->>DH: plugins data
  
  DH->>DH: render plugin cards with example chips
  User->>User: click example chip
  User->>DH: tryPlugin(plugin, example)
  DH->>DH: buildExamplePrompt(example)
  DH->>DH: emit try-item {tryKey, prompt}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A rabbit hops through catalog rows,
With pinned examples showing below,
Cache spins as preloads flow,
Each chip tries its special show,
Smarter sorting—watch it grow!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 PR title 'Make directory examples actionable' directly aligns with the main objective: making example chips actionable by launching task-specific prompts instead of generic ones.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/directory-action-examples

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

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Make directory examples actionable with caching and popular ordering

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add caching layer for directory catalogs (plugins, apps, Composio)
• Implement concrete example chips with task-specific prompts
• Pin popular casual-user ordering for top 20 integrations
• Preload catalog data in background on app startup
• Replace generic "Try it!" buttons with actionable example prompts
Diagram
flowchart LR
  A["Directory Catalogs<br/>plugins/apps/composio"] -->|"cache layer"| B["DirectoryCache<br/>promise + value"]
  B -->|"preload on startup"| C["Background Load<br/>1.5s delay"]
  C -->|"reuse cached data"| D["Skills Tab<br/>fast initial load"]
  E["Example Rules<br/>domain-specific"] -->|"generate chips"| F["Example Chips<br/>5 per card"]
  F -->|"task-specific prompt"| G["Try Example<br/>concrete task"]
  H["Popular Rankings<br/>top 20 pinned"] -->|"sort first"| I["Catalog Sort<br/>pinned + heuristic"]
  I -->|"display order"| J["Directory Cards<br/>popular first"]
Loading

Grey Divider

File Changes

1. src/api/codexGateway.ts ✨ Enhancement +160/-43

Add caching layer for directory catalog API calls

• Add DirectoryCacheOptions type and cache maps for plugins, apps, and Composio connectors
• Implement cache key generation and deep cloning functions for cached data
• Wrap listDirectoryPlugins, listDirectoryApps, getDirectoryComposioStatus, and
 listDirectoryComposioConnectors with caching logic
• Add clearDirectoryCatalogCaches() function called on plugin install/uninstall/enable changes
• Support force-refresh option to bypass cache when needed

src/api/codexGateway.ts


2. src/components/content/directoryHubUtils.ts ✨ Enhancement +48/-3

Pin popular Composio connector ordering and boost scoring

• Add POPULAR_COMPOSIO_TOP_20 pinned ranking array for casual-user popular connectors
• Implement normalizePopularRankName() and pinnedPopularRank() functions
• Update composioPopularScore() to prioritize pinned top 20 entries with 1M+ score boost
• Reorder sort logic to apply popular score before connection rank

src/components/content/directoryHubUtils.ts


3. src/App.vue ✨ Enhancement +27/-1

Preload directory catalogs in background on app startup

• Import directory catalog functions for preloading
• Add preloadDirectoryCatalogs() function called 1.5s after mount
• Update getDirectoryTryItemKey() to use tryKey prop for example-specific tracking
• Add tryKey field to DirectoryTryItemPayload type

src/App.vue


View more (2)
4. src/components/content/DirectoryHub.vue ✨ Enhancement +427/-102

Implement concrete example chips with task-specific prompts

• Add POPULAR_PLUGIN_TOP_20 and POPULAR_APP_TOP_20 pinned ranking arrays
• Implement pluginExampleChips(), appExampleChips(), composioExampleChips() functions
• Add PLUGIN_EXAMPLE_RULES with domain-specific example prompts for 20+ integration types
• Implement exampleChipsFromSource() to generate 5 concrete examples per card
• Replace generic category/capability chips with actionable example buttons
• Add canTryPlugin(), canTryApp(), canTryComposio() functions to check if examples are
 runnable
• Update tryPlugin(), tryApp(), tryComposio() to accept example parameter and build
 task-specific prompts
• Add example chips to plugin detail modal
• Add example chips to Composio detail modal
• Update pluginPopularScore() and appPopularScore() to use pinned top 20 rankings
• Update filter functions to include example chips in search
• Add loadPlugins() and loadApps() deduplication with promise tracking
• Update loadComposio() to support force-refresh parameter
• Change plugin cards from buttons to articles with separate action buttons
• Add styling for .directory-example-chip with blue theme and disabled state
• Add dark theme styles for example chips

src/components/content/DirectoryHub.vue


5. tests.md 🧪 Tests +42/-0

Add test case for plugin catalog example chips

• Add comprehensive test case for plugin catalog example chips feature
• Cover light and dark theme testing
• Verify example chips are visible but disabled until installed/enabled
• Verify popular ordering uses hardcoded top 20 with heuristic fallback
• Verify preload cache behavior and manual refresh bypass
• Verify task-specific prompts vs generic prompts
• Verify example chips in detail modals

tests.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 13, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1)

Grey Divider


Action required

1. Queued Composio refresh lost 🐞 Bug ≡ Correctness
Description
When loadComposio is called while a load is already in progress, the queuing logic drops the
original append/force arguments and later calls loadComposio() with defaults. As a result, a
manual refresh (force=true) during an in-flight load may not actually bypass cached data.
Code

src/components/content/DirectoryHub.vue[R1648-1651]

+async function loadComposio(append = false, force = false): Promise<void> {
  if (isLoadingComposio.value) {
    isComposioLoadQueued = true
    return
Evidence
The queuing path stores only a boolean and the replay call omits parameters, while manual refresh
explicitly relies on force=true.

src/components/content/DirectoryHub.vue[1648-1681]
src/components/content/DirectoryHub.vue[1721-1727]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`loadComposio` queues only a boolean when invoked while already loading, then reruns `loadComposio()` with default parameters. This loses the caller’s intent (notably `force=true` from manual refresh), so the subsequent reload may not be forced.

### Issue Context
Manual refresh calls `loadComposio(false, true)`. If the user clicks refresh during an in-flight request, the queued reload becomes `loadComposio()` (force=false).

### Fix Focus Areas
- src/components/content/DirectoryHub.vue[1648-1681]
- src/components/content/DirectoryHub.vue[1721-1727]

### Implementation notes
- Replace `isComposioLoadQueued` with queued parameters, e.g.:
 - `let queuedComposioAppend = false; let queuedComposioForce = false;`
 - When queuing, merge: `queuedComposioAppend ||= append; queuedComposioForce ||= force`.
 - In finally, call `void loadComposio(queuedComposioAppend, queuedComposioForce)` and reset the queued flags.
- Alternatively use a request-id approach and always re-run if a newer request was requested mid-flight.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Dark chip override not global 📘 Rule violation ⚙ Maintainability
Description
The new .directory-example-chip dark-theme styling is implemented as a component-scoped
:global(:root.dark) override inside DirectoryHub.vue rather than as a decisive global override
in src/style.css. This risks fragmented dark-mode theming across the shared Skills/Directory
surfaces and violates the preference for centralized global dark-theme overrides.
Code

src/components/content/DirectoryHub.vue[R2529-2531]

+:global(:root.dark) .directory-example-chip {
+  @apply border-blue-900/60 bg-blue-950/40 text-blue-200;
+}
Evidence
Rule 4 requires decisive dark-theme overrides for shared route surfaces to live in src/style.css.
The PR adds a new dark-mode override for .directory-example-chip in DirectoryHub.vue and there
is no corresponding global override in src/style.css, indicating the dark-theme fix relies on
component-scoped styling.

AGENTS.md
src/components/content/DirectoryHub.vue[2529-2531]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new dark-theme override for `.directory-example-chip` was added in `src/components/content/DirectoryHub.vue` using `:global(:root.dark) ...`. For shared route surfaces/large UIs, the compliance checklist requires placing decisive dark-theme overrides in the global stylesheet (`src/style.css`) to keep theming consistent and centralized.

## Issue Context
The Skills/Directory UI is a shared route surface; adding new UI elements (example chips) should follow the global dark-theme override approach to avoid scattered component-level theming rules.

## Fix Focus Areas
- src/components/content/DirectoryHub.vue[2529-2531]
- src/style.css[1-220]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Cache refresh race 🐞 Bug ☼ Reliability
Description
The new directory catalog caches write the final value unconditionally inside the request, so an
older in-flight request can overwrite a newer forced refresh and clear its promise, temporarily
serving stale results. This is especially likely now that App.vue preloads catalogs in the
background while DirectoryHub can trigger forced reloads.
Code

src/api/codexGateway.ts[R2156-2178]

+  const request = (async () => {
+    const payload = await callRpc<{ marketplaces?: unknown[] }>('plugin/list', params)
+    const plugins: DirectoryPluginSummary[] = []
+    for (const marketplaceValue of payload.marketplaces ?? []) {
+      const marketplace = asRecord(marketplaceValue)
+      if (!marketplace) continue
+      const iface = asRecord(marketplace.interface)
+      const meta = {
+        name: readString(marketplace.name) ?? '',
+        displayName: readString(iface?.displayName ?? iface?.display_name) ?? '',
+        path: readString(marketplace.path),
+      }
+      const rows = Array.isArray(marketplace.plugins) ? marketplace.plugins : []
+      for (const row of rows) {
+        const plugin = normalizeDirectoryPluginSummary(row, meta)
+        if (plugin) plugins.push(plugin)
+      }
    }
-    const rows = Array.isArray(marketplace.plugins) ? marketplace.plugins : []
-    for (const row of rows) {
-      const plugin = normalizeDirectoryPluginSummary(row, meta)
-      if (plugin) plugins.push(plugin)
+    directoryPluginCache.set(cacheKey, { promise: null, value: cloneDirectoryPluginRows(plugins) })
+    return plugins
+  })()
+  directoryPluginCache.set(cacheKey, { promise: request, value: options.force ? null : cached?.value ?? null })
+  try {
Evidence
Each cache sets its final value inside the async request without checking request identity, while
App.vue now triggers background preloads that can overlap with DirectoryHub forced reloads (manual
refresh/mutations).

src/api/codexGateway.ts[2148-2185]
src/api/codexGateway.ts[2241-2274]
src/api/codexGateway.ts[2314-2334]
src/api/codexGateway.ts[2337-2379]
src/App.vue[4165-4177]
src/components/content/DirectoryHub.vue[1721-1727]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Directory catalog caches (plugins/apps/Composio status + connectors) update the cache on success without verifying that the completing request is still the *current* request stored in the cache entry. If a second request (often `force: true`) starts while an earlier request is in-flight, the earlier request may finish later and overwrite the cache entry (including clearing `promise`), causing stale reads until the newer request completes.

### Issue Context
This PR added background preloading from `App.vue`, increasing the likelihood of overlapping requests with user-triggered `force` reloads.

### Fix Focus Areas
- src/api/codexGateway.ts[2148-2185]
- src/api/codexGateway.ts[2241-2274]
- src/api/codexGateway.ts[2314-2334]
- src/api/codexGateway.ts[2337-2379]

### Implementation notes
- Before writing `{ promise: null, value: ... }` on success, check the cache entry still points to the same `request` promise (e.g., `if (cache.get(key)?.promise === request) { ... }`).
- Apply the same pattern to apps, composio status, and composio connector page caches.
- Consider keeping the `promise` until the *latest* request resolves, and ignoring out-of-order completions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. In-flight loads ignore key 🐞 Bug ≡ Correctness
Description
loadPlugins/loadApps return an existing in-flight promise without checking whether
cwd/threadId or force intent changed, so a prop change or a forced reload while a request is
running can be dropped and populate state for the wrong key. This can leave the UI showing a
stale/wrong catalog until a later refresh.
Code

src/components/content/DirectoryHub.vue[R1602-1646]

+async function loadPlugins(force = false): Promise<void> {
  if (!supportsPlugins.value) return
-  isLoadingPlugins.value = true
-  pluginError.value = ''
-  try {
-    const cwd = props.cwd?.trim()
-    const [nextPlugins] = await Promise.all([
-      listDirectoryPlugins(cwd ? [cwd] : undefined),
-      supportsApps.value ? loadApps() : Promise.resolve(),
-    ])
-    plugins.value = nextPlugins
-  } catch (error) {
-    pluginError.value = error instanceof Error ? error.message : 'Failed to load plugins'
-  } finally {
-    isLoadingPlugins.value = false
-  }
+  const key = props.cwd?.trim() || ''
+  if (!force && pluginsLoadedKey === key && plugins.value.length > 0 && !pluginError.value) return
+  if (pluginsLoadPromise) return pluginsLoadPromise
+  pluginsLoadPromise = (async () => {
+    isLoadingPlugins.value = true
+    pluginError.value = ''
+    try {
+      const [nextPlugins] = await Promise.all([
+        listDirectoryPlugins(key ? [key] : undefined, { force }),
+        supportsApps.value ? loadApps(force) : Promise.resolve(),
+      ])
+      plugins.value = nextPlugins
+      pluginsLoadedKey = key
+    } catch (error) {
+      pluginError.value = error instanceof Error ? error.message : 'Failed to load plugins'
+    } finally {
+      isLoadingPlugins.value = false
+      pluginsLoadPromise = null
+    }
+  })()
+  return pluginsLoadPromise
}

-async function loadApps(): Promise<void> {
+async function loadApps(force = false): Promise<void> {
  if (!supportsApps.value) return
-  isLoadingApps.value = true
-  appError.value = ''
-  try {
-    apps.value = await listDirectoryApps(props.threadId?.trim() || undefined)
-  } catch (error) {
-    appError.value = error instanceof Error ? error.message : 'Failed to load apps'
-  } finally {
-    isLoadingApps.value = false
-  }
+  const key = props.threadId?.trim() || ''
+  if (!force && appsLoadedKey === key && apps.value.length > 0 && !appError.value) return
+  if (appsLoadPromise) return appsLoadPromise
+  appsLoadPromise = (async () => {
+    isLoadingApps.value = true
+    appError.value = ''
+    try {
+      apps.value = await listDirectoryApps(key || undefined, { force })
+      appsLoadedKey = key
+    } catch (error) {
+      appError.value = error instanceof Error ? error.message : 'Failed to load apps'
+    } finally {
+      isLoadingApps.value = false
+      appsLoadPromise = null
+    }
+  })()
+  return appsLoadPromise
}
Evidence
The load functions short-circuit to a shared promise, while watchers and manual refresh can request
reloads based on changing cwd/threadId or force, which can occur during an in-flight request.

src/components/content/DirectoryHub.vue[1602-1646]
src/components/content/DirectoryHub.vue[1987-1992]
src/components/content/DirectoryHub.vue[1721-1727]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
DirectoryHub de-duplicates loads with a single `pluginsLoadPromise` / `appsLoadPromise`. If `cwd`/`threadId` changes (or a `force` reload is requested) while a load is in-flight, the new request returns the old promise and the state update at completion can reflect the old key.

### Issue Context
Watchers trigger reloads on `cwd`/`threadId` changes, and the UI has explicit "Refresh" and mutation flows that request `force` reloads.

### Fix Focus Areas
- src/components/content/DirectoryHub.vue[1602-1646]
- src/components/content/DirectoryHub.vue[1711-1727]
- src/components/content/DirectoryHub.vue[1987-1992]

### Implementation notes
Choose one:
- Track the in-flight request key + force (e.g., `pluginsLoadKeyInFlight`) and only reuse the promise when the requested key/force matches.
- Or keep a monotonically increasing request id; only apply results to `plugins.value/apps.value` when the completing request id is still the latest.
- If a new request arrives mid-flight, queue it (including `force`) and run again after completion.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/content/DirectoryHub.vue (2)

1855-1860: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Force the post-install Composio refresh.

This path still calls loadComposio() without force. Right after installation, the cached “not installed” status can be reused, leaving the empty state onscreen until the user refreshes manually.

♻️ Suggested fix
   try {
     await installDirectoryComposioCli()
     showToast('Composio CLI installed')
-    await loadComposio()
+    await loadComposio(false, true)
   } catch (error) {
🤖 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 `@src/components/content/DirectoryHub.vue` around lines 1855 - 1860, The
installComposioCli flow sets isInstallingComposio but then calls loadComposio()
without forcing a reload, which can reuse a cached “not installed” state; update
installComposioCli to call loadComposio with the force flag (e.g.,
loadComposio(true) or whatever the loadComposio API expects) immediately after
installDirectoryComposioCli() completes so the post-install status is refreshed;
ensure any related cleanup (isInstallingComposio.value toggling) remains intact.

1648-1680: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve queued force reloads for Composio.

When loadComposio() is called during an existing load, you only set isComposioLoadQueued = true, then replay with loadComposio() in finally. That drops the queued force=true intent, so manual refresh / post-connect refresh can silently fall back to cached data.

♻️ Suggested fix
 let composioSearchTimer: ReturnType<typeof setTimeout> | null = null
 let isComposioLoadQueued = false
+let queuedComposioAppend = false
+let queuedComposioForce = false

 async function loadComposio(append = false, force = false): Promise<void> {
   if (isLoadingComposio.value) {
     isComposioLoadQueued = true
+    queuedComposioAppend = queuedComposioAppend || append
+    queuedComposioForce = queuedComposioForce || force
     return
   }
@@
   } finally {
     isLoadingComposio.value = false
     if (isComposioLoadQueued) {
       isComposioLoadQueued = false
-      void loadComposio()
+      const nextAppend = queuedComposioAppend
+      const nextForce = queuedComposioForce
+      queuedComposioAppend = false
+      queuedComposioForce = false
+      void loadComposio(nextAppend, nextForce)
     }
   }
 }
🤖 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 `@src/components/content/DirectoryHub.vue` around lines 1648 - 1680, The
current loadComposio function drops the caller's force=true intent when a load
is already in progress because isComposioLoadQueued is a simple boolean; modify
loadComposio to record the requested flags (at least a queuedForce boolean, and
queuedAppend if needed) when setting isComposioLoadQueued, and when replaying in
the finally block call loadComposio(queuedAppend, queuedForce) (then clear
queued flags and isComposioLoadQueued). Update references in loadComposio, the
isComposioLoadQueued usage, and the finally replay logic so the original force
parameter is preserved on queued retries.
🤖 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 `@src/api/codexGateway.ts`:
- Around line 2108-2111: The Maps directoryPluginCache, directoryAppCache and
directoryComposioConnectorCache are unbounded and can grow indefinitely
(especially directoryComposioConnectorCache because keys include free-form
query/cursor/limit); add a bounded cache helper (e.g., setBoundedCacheEntry)
that accepts a Map, a normalized key, the value/promise and a maxSize,
implements simple eviction (LRU or FIFO) when size > maxSize, and use it to
replace direct map.set(...) calls for directoryPluginCache, directoryAppCache
and directoryComposioConnectorCache; also normalize keys for
directoryComposioConnectorCache (strip or canonicalize cursor/limit/query
params) before storing so ephemeral pagination keys don’t bloat the cache, and
keep directoryComposioStatusCache as a single-entry object or wrap it with the
same helper if desired.

In `@src/App.vue`:
- Line 4482: The dedupe key generation currently uses the nullish coalescing on
payload.tryKey so empty strings are accepted; change the expression in the
return statement that builds the key (the payload.tryKey ?? ...) to treat
blank/whitespace tryKey as absent by checking payload.tryKey?.trim() and using
it only when non-empty, e.g. use (payload.tryKey?.trim() ? payload.tryKey :
`<rest of template>`), so directoryTryInFlightKey won’t receive empty-string
keys.
- Around line 2018-2020: Store the timeout handle returned by window.setTimeout
when scheduling preloadDirectoryCatalogs (e.g., const preloadTimer =
window.setTimeout(...)) and register a Vue unmount hook (onBeforeUnmount or
beforeUnmount in the component setup) that calls clearTimeout(preloadTimer) to
cancel the scheduled callback; this ensures preloadDirectoryCatalogs cannot run
after the component has unmounted.

In `@src/components/content/DirectoryHub.vue`:
- Around line 2235-2237: Remove the redundant top-auto margin from the chip row:
.directory-example-chip-row should not apply mt-auto because
.directory-card-actions already uses mt-auto, which causes flexbox to split
space and create gaps; locate the CSS rule for .directory-example-chip-row and
delete the `@apply` mt-auto (or replace it with a non-auto margin/spacing as
needed) so only .directory-card-actions controls the auto spacing.
- Around line 2529-2531: The dark-mode override for .directory-example-chip is
forcing active blue styles onto disabled chips; update the CSS so the rule only
targets enabled chips or explicitly reapply disabled styling for disabled chips.
For example, change the selector that uses :root.dark .directory-example-chip to
exclude disabled states (e.g. :root.dark
.directory-example-chip:not([disabled]):not([aria-disabled="true"]):not(.disabled))
or add a specific rule under :root.dark for
.directory-example-chip[aria-disabled="true"], .directory-example-chip.disabled,
and .directory-example-chip[disabled] to restore the muted/disabled colors;
reference the .directory-example-chip selector and the :root.dark scope when
making the change.
- Around line 1602-1624: The in-flight dedupe must include the request context
so a mid-flight cwd/threadId/force change doesn't let an old response overwrite
new state: compute a requestKey (e.g. combine props.cwd?.trim() || '', current
threadId, and force) at the start of loadPlugins and use that key to gate both
deduping and result application; replace the single pluginsLoadPromise with a
keyed store (e.g. a Map<string, Promise<void>> or keep a local requestPromise
per requestKey), return the promise for that requestKey, and before setting
plugins.value and pluginsLoadedKey only apply results if the still-current
requestKey matches (or remove the map entry once done) so each distinct context
gets its own in-flight promise and stale responses cannot repopulate the
catalog.

---

Outside diff comments:
In `@src/components/content/DirectoryHub.vue`:
- Around line 1855-1860: The installComposioCli flow sets isInstallingComposio
but then calls loadComposio() without forcing a reload, which can reuse a cached
“not installed” state; update installComposioCli to call loadComposio with the
force flag (e.g., loadComposio(true) or whatever the loadComposio API expects)
immediately after installDirectoryComposioCli() completes so the post-install
status is refreshed; ensure any related cleanup (isInstallingComposio.value
toggling) remains intact.
- Around line 1648-1680: The current loadComposio function drops the caller's
force=true intent when a load is already in progress because
isComposioLoadQueued is a simple boolean; modify loadComposio to record the
requested flags (at least a queuedForce boolean, and queuedAppend if needed)
when setting isComposioLoadQueued, and when replaying in the finally block call
loadComposio(queuedAppend, queuedForce) (then clear queued flags and
isComposioLoadQueued). Update references in loadComposio, the
isComposioLoadQueued usage, and the finally replay logic so the original force
parameter is preserved on queued retries.
🪄 Autofix (Beta)

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: 38f8d0ce-0da7-44ce-b333-93a5d48406c1

📥 Commits

Reviewing files that changed from the base of the PR and between 1c9dacd and 80d2694.

📒 Files selected for processing (5)
  • src/App.vue
  • src/api/codexGateway.ts
  • src/components/content/DirectoryHub.vue
  • src/components/content/directoryHubUtils.ts
  • tests.md

Comment thread src/api/codexGateway.ts
Comment on lines +2108 to +2111
const directoryPluginCache = new Map<string, { promise: Promise<DirectoryPluginSummary[]> | null; value: DirectoryPluginSummary[] | null }>()
const directoryAppCache = new Map<string, { promise: Promise<DirectoryAppInfo[]> | null; value: DirectoryAppInfo[] | null }>()
const directoryComposioConnectorCache = new Map<string, { promise: Promise<DirectoryComposioConnectorPage> | null; value: DirectoryComposioConnectorPage | null }>()
let directoryComposioStatusCache: { promise: Promise<DirectoryComposioStatus> | null; value: DirectoryComposioStatus | null } = { promise: null, value: null }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bound cache growth to avoid long-session memory bloat.

These Maps are unbounded, and directoryComposioConnectorCache keys include free-form query/cursor/limit. In active sessions (search typing, pagination), entries can accumulate indefinitely.

💡 Suggested direction (bounded cache helper)
+const DIRECTORY_CACHE_MAX_ENTRIES = 200
+
+function setBoundedCacheEntry<K, V>(map: Map<K, V>, key: K, value: V): void {
+  if (map.has(key)) map.delete(key)
+  map.set(key, value)
+  if (map.size > DIRECTORY_CACHE_MAX_ENTRIES) {
+    const oldestKey = map.keys().next().value as K | undefined
+    if (oldestKey !== undefined) map.delete(oldestKey)
+  }
+}

Then replace map.set(...) calls in these cache paths with setBoundedCacheEntry(...).

🤖 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 `@src/api/codexGateway.ts` around lines 2108 - 2111, The Maps
directoryPluginCache, directoryAppCache and directoryComposioConnectorCache are
unbounded and can grow indefinitely (especially directoryComposioConnectorCache
because keys include free-form query/cursor/limit); add a bounded cache helper
(e.g., setBoundedCacheEntry) that accepts a Map, a normalized key, the
value/promise and a maxSize, implements simple eviction (LRU or FIFO) when size
> maxSize, and use it to replace direct map.set(...) calls for
directoryPluginCache, directoryAppCache and directoryComposioConnectorCache;
also normalize keys for directoryComposioConnectorCache (strip or canonicalize
cursor/limit/query params) before storing so ephemeral pagination keys don’t
bloat the cache, and keep directoryComposioStatusCache as a single-entry object
or wrap it with the same helper if desired.

Comment thread src/App.vue
Comment on lines +2018 to +2020
window.setTimeout(() => {
void preloadDirectoryCatalogs()
}, 1500)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear the scheduled preload timer on unmount.

Line 2018 schedules a timeout but the handle is not retained, so the callback can still fire after unmount and issue background requests from a stale lifecycle.

🧩 Proposed fix
@@
 let threadSearchTimer: ReturnType<typeof setTimeout> | null = null
 let terminalKeyboardFocusFallbackTimer: ReturnType<typeof setTimeout> | null = null
+let preloadDirectoryCatalogsTimer: ReturnType<typeof setTimeout> | null = null
@@
-  window.setTimeout(() => {
+  preloadDirectoryCatalogsTimer = window.setTimeout(() => {
     void preloadDirectoryCatalogs()
   }, 1500)
@@
   if (threadSearchTimer) {
     clearTimeout(threadSearchTimer)
     threadSearchTimer = null
   }
+  if (preloadDirectoryCatalogsTimer) {
+    clearTimeout(preloadDirectoryCatalogsTimer)
+    preloadDirectoryCatalogsTimer = null
+  }
   clearTerminalKeyboardFocusFallbackTimer()
🤖 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 `@src/App.vue` around lines 2018 - 2020, Store the timeout handle returned by
window.setTimeout when scheduling preloadDirectoryCatalogs (e.g., const
preloadTimer = window.setTimeout(...)) and register a Vue unmount hook
(onBeforeUnmount or beforeUnmount in the component setup) that calls
clearTimeout(preloadTimer) to cancel the scheduled callback; this ensures
preloadDirectoryCatalogs cannot run after the component has unmounted.

Comment thread src/App.vue

function getDirectoryTryItemKey(payload: DirectoryTryItemPayload): string {
return `${payload.kind}:${payload.name}:${payload.skillPath ?? ''}`
return payload.tryKey ?? `${payload.kind}:${payload.name}:${payload.skillPath ?? ''}:${payload.prompt ?? ''}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against blank tryKey values in dedupe key generation.

Line 4482 uses ??, so an empty string tryKey is treated as a valid key. That can break the in-flight guard (directoryTryInFlightKey) behavior for rapid repeated clicks.

🧩 Proposed fix
 function getDirectoryTryItemKey(payload: DirectoryTryItemPayload): string {
-  return payload.tryKey ?? `${payload.kind}:${payload.name}:${payload.skillPath ?? ''}:${payload.prompt ?? ''}`
+  const explicitTryKey = payload.tryKey?.trim()
+  return explicitTryKey && explicitTryKey.length > 0
+    ? explicitTryKey
+    : `${payload.kind}:${payload.name}:${payload.skillPath ?? ''}:${payload.prompt ?? ''}`
 }
📝 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.

Suggested change
return payload.tryKey ?? `${payload.kind}:${payload.name}:${payload.skillPath ?? ''}:${payload.prompt ?? ''}`
function getDirectoryTryItemKey(payload: DirectoryTryItemPayload): string {
const explicitTryKey = payload.tryKey?.trim()
return explicitTryKey && explicitTryKey.length > 0
? explicitTryKey
: `${payload.kind}:${payload.name}:${payload.skillPath ?? ''}:${payload.prompt ?? ''}`
}
🤖 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 `@src/App.vue` at line 4482, The dedupe key generation currently uses the
nullish coalescing on payload.tryKey so empty strings are accepted; change the
expression in the return statement that builds the key (the payload.tryKey ??
...) to treat blank/whitespace tryKey as absent by checking
payload.tryKey?.trim() and using it only when non-empty, e.g. use
(payload.tryKey?.trim() ? payload.tryKey : `<rest of template>`), so
directoryTryInFlightKey won’t receive empty-string keys.

Comment on lines +1602 to +1624
async function loadPlugins(force = false): Promise<void> {
if (!supportsPlugins.value) return
isLoadingPlugins.value = true
pluginError.value = ''
try {
const cwd = props.cwd?.trim()
const [nextPlugins] = await Promise.all([
listDirectoryPlugins(cwd ? [cwd] : undefined),
supportsApps.value ? loadApps() : Promise.resolve(),
])
plugins.value = nextPlugins
} catch (error) {
pluginError.value = error instanceof Error ? error.message : 'Failed to load plugins'
} finally {
isLoadingPlugins.value = false
}
const key = props.cwd?.trim() || ''
if (!force && pluginsLoadedKey === key && plugins.value.length > 0 && !pluginError.value) return
if (pluginsLoadPromise) return pluginsLoadPromise
pluginsLoadPromise = (async () => {
isLoadingPlugins.value = true
pluginError.value = ''
try {
const [nextPlugins] = await Promise.all([
listDirectoryPlugins(key ? [key] : undefined, { force }),
supportsApps.value ? loadApps(force) : Promise.resolve(),
])
plugins.value = nextPlugins
pluginsLoadedKey = key
} catch (error) {
pluginError.value = error instanceof Error ? error.message : 'Failed to load plugins'
} finally {
isLoadingPlugins.value = false
pluginsLoadPromise = null
}
})()
return pluginsLoadPromise
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Key the in-flight dedupe by request context.

Both loaders return the current promise even when cwd, threadId, or force changed after the request started. If the user switches threads/directories mid-flight, the new load is dropped and the old response repopulates the catalog with stale data.

Also applies to: 1627-1645

🤖 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 `@src/components/content/DirectoryHub.vue` around lines 1602 - 1624, The
in-flight dedupe must include the request context so a mid-flight
cwd/threadId/force change doesn't let an old response overwrite new state:
compute a requestKey (e.g. combine props.cwd?.trim() || '', current threadId,
and force) at the start of loadPlugins and use that key to gate both deduping
and result application; replace the single pluginsLoadPromise with a keyed store
(e.g. a Map<string, Promise<void>> or keep a local requestPromise per
requestKey), return the promise for that requestKey, and before setting
plugins.value and pluginsLoadedKey only apply results if the still-current
requestKey matches (or remove the map entry once done) so each distinct context
gets its own in-flight promise and stale responses cannot repopulate the
catalog.

Comment on lines +2235 to +2237
.directory-example-chip-row {
@apply mt-auto;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid a second auto-margin on the card body.

.directory-card-actions already uses mt-auto. Adding mt-auto to .directory-example-chip-row makes flexbox split the remaining space between the chip row and the footer, which creates awkward vertical gaps in taller cards.

♻️ Suggested fix
 .directory-example-chip-row {
-  `@apply` mt-auto;
+  `@apply` pt-1;
 }
📝 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.

Suggested change
.directory-example-chip-row {
@apply mt-auto;
}
.directory-example-chip-row {
`@apply` pt-1;
}
🤖 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 `@src/components/content/DirectoryHub.vue` around lines 2235 - 2237, Remove the
redundant top-auto margin from the chip row: .directory-example-chip-row should
not apply mt-auto because .directory-card-actions already uses mt-auto, which
causes flexbox to split space and create gaps; locate the CSS rule for
.directory-example-chip-row and delete the `@apply` mt-auto (or replace it with a
non-auto margin/spacing as needed) so only .directory-card-actions controls the
auto spacing.

Comment on lines +2529 to +2531
:global(:root.dark) .directory-example-chip {
@apply border-blue-900/60 bg-blue-950/40 text-blue-200;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep disabled chips visibly disabled in dark mode.

This override reapplies the active blue palette to every example chip, including disabled ones. In dark mode the buttons remain non-clickable, but they stop looking disabled, which undercuts the new “visible but unavailable” behavior.

♻️ Suggested fix
-:global(:root.dark) .directory-example-chip {
+:global(:root.dark) .directory-example-chip:enabled {
   `@apply` border-blue-900/60 bg-blue-950/40 text-blue-200;
 }
+
+:global(:root.dark) .directory-example-chip:disabled {
+  `@apply` border-zinc-700 bg-zinc-800 text-zinc-500;
+}
🤖 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 `@src/components/content/DirectoryHub.vue` around lines 2529 - 2531, The
dark-mode override for .directory-example-chip is forcing active blue styles
onto disabled chips; update the CSS so the rule only targets enabled chips or
explicitly reapply disabled styling for disabled chips. For example, change the
selector that uses :root.dark .directory-example-chip to exclude disabled states
(e.g. :root.dark
.directory-example-chip:not([disabled]):not([aria-disabled="true"]):not(.disabled))
or add a specific rule under :root.dark for
.directory-example-chip[aria-disabled="true"], .directory-example-chip.disabled,
and .directory-example-chip[disabled] to restore the muted/disabled colors;
reference the .directory-example-chip selector and the :root.dark scope when
making the change.

Comment on lines +1648 to 1651
async function loadComposio(append = false, force = false): Promise<void> {
if (isLoadingComposio.value) {
isComposioLoadQueued = true
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Queued composio refresh lost 🐞 Bug ≡ Correctness

When loadComposio is called while a load is already in progress, the queuing logic drops the
original append/force arguments and later calls loadComposio() with defaults. As a result, a
manual refresh (force=true) during an in-flight load may not actually bypass cached data.
Agent Prompt
### Issue description
`loadComposio` queues only a boolean when invoked while already loading, then reruns `loadComposio()` with default parameters. This loses the caller’s intent (notably `force=true` from manual refresh), so the subsequent reload may not be forced.

### Issue Context
Manual refresh calls `loadComposio(false, true)`. If the user clicks refresh during an in-flight request, the queued reload becomes `loadComposio()` (force=false).

### Fix Focus Areas
- src/components/content/DirectoryHub.vue[1648-1681]
- src/components/content/DirectoryHub.vue[1721-1727]

### Implementation notes
- Replace `isComposioLoadQueued` with queued parameters, e.g.:
  - `let queuedComposioAppend = false; let queuedComposioForce = false;`
  - When queuing, merge: `queuedComposioAppend ||= append; queuedComposioForce ||= force`.
  - In finally, call `void loadComposio(queuedComposioAppend, queuedComposioForce)` and reset the queued flags.
- Alternatively use a request-id approach and always re-run if a newer request was requested mid-flight.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants