From d7fb6776bd09edbf07c2a898e40f5d8f1386c96f Mon Sep 17 00:00:00 2001
From: Igor
Date: Wed, 13 May 2026 08:03:37 +0700
Subject: [PATCH 1/8] Add plugin catalog example chips
---
src/components/content/DirectoryHub.vue | 219 ++++++++++++++++++++----
tests.md | 30 ++++
2 files changed, 211 insertions(+), 38 deletions(-)
diff --git a/src/components/content/DirectoryHub.vue b/src/components/content/DirectoryHub.vue
index beff5537a..02cb0ff26 100644
--- a/src/components/content/DirectoryHub.vue
+++ b/src/components/content/DirectoryHub.vue
@@ -101,9 +101,8 @@
{{ plugin.description }}
-
-
{{ plugin.category }}
-
{{ capability }}
+
+ {{ example }}
@@ -449,6 +448,13 @@
{{ selectedPluginDescription }}
+
+
Examples
+
+ {{ example }}
+
+
+
Capabilities
@@ -733,8 +739,62 @@ const POPULAR_APP_KEYWORD_BONUSES: Array<[RegExp, number]> = [
[/(task|project|issue|repository|deploy|database|crm|sales|support)/i, 120],
]
const POPULAR_PLUGIN_NAME_BONUSES: Array<[RegExp, number]> = [
- [/(computer use|github|gitlab|linear|slack|notion|browser|web|filesystem|terminal)/i, 120],
- [/(calendar|email|drive|docs|design|deploy|project|issue|search|database)/i, 55],
+ [/(browser|chrome|computer use|github|gitlab|linear|slack|notion|gmail|email|calendar|drive|figma|canva|cloudflare|terminal|filesystem)/i, 180],
+ [/(web|docs|design|deploy|project|issue|search|database|outlook|teams|sharepoint|jira|vercel|netlify)/i, 75],
+]
+const PLUGIN_EXAMPLE_RULES: Array<{ pattern: RegExp; examples: string[] }> = [
+ {
+ pattern: /(browser|web)/i,
+ examples: ['Open a local URL', 'Click through a flow', 'Capture a screenshot', 'Inspect visible text', 'Verify mobile layout'],
+ },
+ {
+ pattern: /(chrome)/i,
+ examples: ['Use my Chrome session', 'Inspect current tab', 'Test logged-in page', 'Capture page state', 'Check extension UI'],
+ },
+ {
+ pattern: /(computer use|desktop)/i,
+ examples: ['Control a desktop app', 'Read window state', 'Click a toolbar', 'Fill a form', 'Verify a dialog'],
+ },
+ {
+ pattern: /(github|gitlab|repository|pull request|issue)/i,
+ examples: ['Review a pull request', 'Triage open issues', 'Check CI failures', 'Summarize commits', 'Draft a PR comment'],
+ },
+ {
+ pattern: /(linear|jira|asana|trello|clickup|project|task)/i,
+ examples: ['Find assigned issues', 'Summarize blockers', 'Create a task', 'Update issue status', 'Plan next sprint'],
+ },
+ {
+ pattern: /(slack|teams|chat|message)/i,
+ examples: ['Summarize a channel', 'Find mentions', 'Draft a reply', 'Search a thread', 'Post an update'],
+ },
+ {
+ pattern: /(notion|docs|document|wiki|page)/i,
+ examples: ['Find a page', 'Summarize docs', 'Create meeting notes', 'Update a wiki page', 'Extract action items'],
+ },
+ {
+ pattern: /(gmail|email|outlook|mail|inbox)/i,
+ examples: ['Summarize unread mail', 'Find an email', 'Draft a reply', 'Check attachments', 'Archive newsletters'],
+ },
+ {
+ pattern: /(calendar|event|availability|meeting)/i,
+ examples: ['Find free time', 'Summarize today', 'Create a meeting', 'Move an event', 'Check attendee status'],
+ },
+ {
+ pattern: /(drive|dropbox|box|sharepoint|file|storage)/i,
+ examples: ['Find a file', 'Read a document', 'Summarize a folder', 'Check recent edits', 'Share a file'],
+ },
+ {
+ pattern: /(figma|canva|design|image|slide|presentation)/i,
+ examples: ['Inspect a design', 'Create a mockup', 'Export assets', 'Compare variants', 'Draft presentation copy'],
+ },
+ {
+ pattern: /(cloudflare|vercel|netlify|deploy|worker|hosting)/i,
+ examples: ['Check deploy status', 'List recent builds', 'Inspect DNS records', 'Review logs', 'Publish an update'],
+ },
+ {
+ pattern: /(terminal|shell|filesystem|file system|database|postgres|sqlite)/i,
+ examples: ['Run a safe command', 'Search project files', 'Inspect database rows', 'Check disk usage', 'Summarize logs'],
+ },
]
const POPULAR_MCP_NAME_BONUSES: Array<[RegExp, number]> = [
[/(github|gitlab|linear|slack|notion|filesystem|browser|computer|web|postgres|sqlite|database)/i, 120],
@@ -877,6 +937,10 @@ const toast = ref<{ text: string; type: 'success' | 'error' } | null>(null)
let toastTimer: ReturnType | null = null
let composioSearchTimer: ReturnType | null = null
let isComposioLoadQueued = false
+let pluginsLoadPromise: Promise | null = null
+let appsLoadPromise: Promise | null = null
+let pluginsLoadedKey = ''
+let appsLoadedKey = ''
const activeCopy = computed(() => tabs.find((tab) => tab.id === activeTab.value) ?? tabs[0])
const supportsPlugins = computed(() =>
@@ -1002,6 +1066,56 @@ function includesSearch(parts: Array, query: string):
return parts.some((part) => part?.toLowerCase().includes(normalized))
}
+function addExampleChip(chips: string[], seen: Set, value: string): void {
+ const chip = value.trim()
+ if (!chip) return
+ const key = chip.toLowerCase()
+ if (seen.has(key)) return
+ seen.add(key)
+ chips.push(chip)
+}
+
+function pluginExampleSource(plugin: DirectoryPluginSummary): string {
+ return [
+ plugin.displayName,
+ plugin.name,
+ plugin.description,
+ plugin.longDescription,
+ plugin.category,
+ plugin.developerName,
+ plugin.capabilities.join(' '),
+ ].join(' ')
+}
+
+function pluginExampleChips(plugin: DirectoryPluginSummary): string[] {
+ const chips: string[] = []
+ const seen = new Set()
+ const source = pluginExampleSource(plugin)
+
+ for (const rule of PLUGIN_EXAMPLE_RULES) {
+ if (!rule.pattern.test(source)) continue
+ for (const example of rule.examples) addExampleChip(chips, seen, example)
+ if (chips.length >= 5) return chips.slice(0, 5)
+ }
+
+ for (const prompt of plugin.defaultPrompt) {
+ addExampleChip(chips, seen, prompt.replace(/[.!?]\s*$/u, ''))
+ if (chips.length >= 5) return chips.slice(0, 5)
+ }
+
+ for (const capability of plugin.capabilities) {
+ addExampleChip(chips, seen, `Try ${capability}`)
+ if (chips.length >= 5) return chips.slice(0, 5)
+ }
+
+ addExampleChip(chips, seen, `Explore ${plugin.displayName}`)
+ addExampleChip(chips, seen, 'Show available actions')
+ addExampleChip(chips, seen, 'Run a safe check')
+ addExampleChip(chips, seen, 'Summarize current state')
+ addExampleChip(chips, seen, 'Suggest next steps')
+ return chips.slice(0, 5)
+}
+
function bonusForName(name: string, rows: Array<[RegExp, number]>): number {
return rows.reduce((score, [pattern, bonus]) => score + (pattern.test(name) ? bonus : 0), 0)
}
@@ -1084,6 +1198,8 @@ function filterPlugins(rows: DirectoryPluginSummary[], query: string): Directory
plugin.category,
plugin.marketplaceDisplayName,
...plugin.capabilities,
+ ...plugin.defaultPrompt,
+ ...pluginExampleChips(plugin),
], query))
}
@@ -1301,35 +1417,50 @@ async function loadMethods(): Promise {
}
}
-async function loadPlugins(): Promise {
+async function loadPlugins(force = false): Promise {
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),
+ 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 {
+async function loadApps(force = false): Promise {
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)
+ appsLoadedKey = key
+ } catch (error) {
+ appError.value = error instanceof Error ? error.message : 'Failed to load apps'
+ } finally {
+ isLoadingApps.value = false
+ appsLoadPromise = null
+ }
+ })()
+ return appsLoadPromise
}
async function loadComposio(append = false): Promise {
@@ -1396,8 +1527,8 @@ async function refreshMcpStatusesForPluginDetail(): Promise {
}
function refreshActiveTab(forceReload = false): void {
- if (activeTab.value === 'plugins') void loadPlugins()
- if (activeTab.value === 'apps') void loadApps()
+ if (activeTab.value === 'plugins') void loadPlugins(forceReload)
+ if (activeTab.value === 'apps') void loadApps(forceReload)
if (activeTab.value === 'composio') void loadComposio()
if (activeTab.value === 'skills') {
if (forceReload && supportsMcpReload.value) void reloadMcps()
@@ -1408,8 +1539,8 @@ function refreshActiveTab(forceReload = false): void {
async function manualRefreshActiveTab(): Promise {
isManualRefreshInFlight.value = true
try {
- if (activeTab.value === 'plugins') await loadPlugins()
- else if (activeTab.value === 'apps') await loadApps()
+ if (activeTab.value === 'plugins') await loadPlugins(true)
+ else if (activeTab.value === 'apps') await loadApps(true)
else if (activeTab.value === 'composio') await loadComposio()
else if (activeTab.value === 'skills' && supportsMcpReload.value) await reloadMcps()
else if (activeTab.value === 'skills') await loadMcps()
@@ -1561,7 +1692,7 @@ async function installSelectedPlugin(): Promise {
installAuthApps.value = result.appsNeedingAuth
showToast(`${selectedPlugin.value.displayName} plugin installed`)
const openedAppLogin = openFirstAppLoginIfNeeded(result.appsNeedingAuth)
- await loadPlugins()
+ await loadPlugins(true)
const updated = plugins.value.find((plugin) => plugin.id === selectedPlugin.value?.id)
if (updated) {
await openPluginDetail(updated)
@@ -1584,7 +1715,7 @@ async function uninstallSelectedPlugin(): Promise {
await uninstallDirectoryPlugin(selectedPlugin.value.id)
showToast(`${name} plugin uninstalled`)
closePluginDetail()
- await loadPlugins()
+ await loadPlugins(true)
} catch (error) {
showToast(error instanceof Error ? error.message : 'Failed to uninstall plugin', 'error')
} finally {
@@ -1606,7 +1737,7 @@ async function toggleSelectedPlugin(): Promise {
}
}
showToast(`${selectedPlugin.value.displayName} plugin ${next ? 'enabled' : 'disabled'}`)
- await loadPlugins()
+ await loadPlugins(true)
} catch (error) {
showToast(error instanceof Error ? error.message : 'Failed to update plugin', 'error')
} finally {
@@ -1672,7 +1803,7 @@ watch(composioSearchQuery, () => {
}, 250)
})
watch(() => props.cwd, () => {
- if (activeTab.value === 'plugins') void loadPlugins()
+ if (activeTab.value === 'plugins') void loadPlugins(true)
})
watch(() => props.threadId, () => {
if (activeTab.value === 'apps' || activeTab.value === 'plugins') void loadApps()
@@ -1919,6 +2050,14 @@ button.directory-card {
@apply rounded-md border border-zinc-200 bg-zinc-50 px-1.5 py-0.5 text-[10px] font-medium text-zinc-500;
}
+.directory-example-chip-row {
+ @apply mt-auto;
+}
+
+.directory-example-chip {
+ @apply border-blue-100 bg-blue-50 text-blue-700;
+}
+
.directory-card-actions {
@apply mt-auto flex items-center gap-2 pt-1;
}
@@ -2205,6 +2344,10 @@ button.directory-card {
@apply border-zinc-700 bg-zinc-800 text-zinc-100;
}
+:global(:root.dark) .directory-example-chip {
+ @apply border-blue-900/60 bg-blue-950/40 text-blue-200;
+}
+
:global(:root.dark) .directory-sort-group {
@apply border-zinc-700 bg-zinc-950;
}
diff --git a/tests.md b/tests.md
index 2558d0dfa..e410c4c94 100644
--- a/tests.md
+++ b/tests.md
@@ -336,6 +336,36 @@ Rollback/cleanup:
---
+### Plugin catalog shows concrete example chips
+
+#### Feature/Change Name
+Plugin catalog popular examples.
+
+#### Prerequisites/Setup
+1. Dev server running (`pnpm run dev --host 127.0.0.1 --port 4173`).
+2. Plugin catalog APIs available from the current Codex CLI.
+3. Light theme and dark theme both available from the appearance switcher.
+
+#### Steps
+1. In light theme, open `/#/skills?tab=plugins`.
+2. Confirm plugin cards are sorted by `Popular` by default.
+3. Confirm each visible plugin card shows five concrete example chips, such as browser, GitHub, email, calendar, file, deploy, or fallback action examples, instead of only generic category/capability chips.
+4. Open an installed plugin detail and confirm the same five example chips are shown in the `Examples` section.
+5. Open a plugin that is not installed or is unavailable for install and confirm its example chips remain visible in the card and detail view.
+6. Search by text from an example chip and confirm matching plugins remain discoverable.
+7. Switch to dark theme and repeat steps 1-6.
+
+#### Expected Results
+- Plugin examples are visible before installation and are not gated by installed/enabled state.
+- Popular plugin families show practical examples tailored to their domain.
+- Plugins without recognized domains still show five fallback examples derived from default prompts, capabilities, or safe generic actions.
+- Example chips remain readable in light theme and dark theme.
+
+#### Rollback/Cleanup
+- None.
+
+---
+
### Qodo feedback diagnostics reliability fixes
#### Feature/Change Name
From 1f627c0ba0b640cdaeefe24ff589a244bfa8a46a Mon Sep 17 00:00:00 2001
From: Igor
Date: Wed, 13 May 2026 08:10:20 +0700
Subject: [PATCH 2/8] Pin popular directory catalog ordering
---
src/components/content/DirectoryHub.vue | 65 +++++++++++++++++++++
src/components/content/directoryHubUtils.ts | 49 +++++++++++++++-
tests.md | 15 +++--
3 files changed, 122 insertions(+), 7 deletions(-)
diff --git a/src/components/content/DirectoryHub.vue b/src/components/content/DirectoryHub.vue
index 02cb0ff26..cde866dc7 100644
--- a/src/components/content/DirectoryHub.vue
+++ b/src/components/content/DirectoryHub.vue
@@ -702,6 +702,50 @@ const COMPOSIO_SKILL_PATH = '/Users/igor/.codex/skills/shared_skills/composio-cl
const COMPOSIO_PAGE_LIMIT = 50
const POPULAR_LIMIT = 100
+const POPULAR_PLUGIN_TOP_20: Array<[RegExp, number]> = [
+ [/^(browser|browser use|in app browser)$/i, 20],
+ [/^chrome$/i, 19],
+ [/^(computer use|desktop)$/i, 18],
+ [/^github$/i, 17],
+ [/^gmail$/i, 16],
+ [/^google calendar$/i, 15],
+ [/^outlook email$/i, 14],
+ [/^google drive$/i, 13],
+ [/^slack$/i, 12],
+ [/^notion$/i, 11],
+ [/^figma$/i, 10],
+ [/^canva$/i, 9],
+ [/^linear$/i, 8],
+ [/^jira$/i, 7],
+ [/^google docs$/i, 6],
+ [/^google sheets$/i, 5],
+ [/^dropbox$/i, 4],
+ [/^(teams|microsoft teams)$/i, 3],
+ [/^sharepoint$/i, 2],
+ [/^cloudflare$/i, 1],
+]
+const POPULAR_APP_TOP_20: Array<[RegExp, number]> = [
+ [/^gmail$/i, 20],
+ [/^google calendar$/i, 19],
+ [/^outlook email$/i, 18],
+ [/^outlook calendar$/i, 17],
+ [/^google drive$/i, 16],
+ [/^google docs$/i, 15],
+ [/^google sheets$/i, 14],
+ [/^dropbox$/i, 13],
+ [/^slack$/i, 12],
+ [/^notion$/i, 11],
+ [/^canva$/i, 10],
+ [/^figma$/i, 9],
+ [/^github$/i, 8],
+ [/^linear$/i, 7],
+ [/^jira$/i, 6],
+ [/^trello$/i, 5],
+ [/^asana$/i, 4],
+ [/^youtube$/i, 3],
+ [/^(x|twitter)$/i, 2],
+ [/^salesforce$/i, 1],
+]
const POPULAR_APP_NAME_BONUSES: Array<[RegExp, number]> = [
[/^gmail$/i, 30_000],
[/^google calendar$/i, 29_500],
@@ -1120,6 +1164,20 @@ function bonusForName(name: string, rows: Array<[RegExp, number]>): number {
return rows.reduce((score, [pattern, bonus]) => score + (pattern.test(name) ? bonus : 0), 0)
}
+function normalizePopularRankName(name: string): string {
+ return normalizeAppNameForRanking(name)
+ .replace(/[-_]+/gu, ' ')
+ .replace(/\s+plugin$/iu, '')
+ .replace(/\s+/gu, ' ')
+ .trim()
+}
+
+function pinnedPopularRank(name: string, rows: Array<[RegExp, number]>): number {
+ const normalized = normalizePopularRankName(name)
+ const match = rows.find(([pattern]) => pattern.test(normalized))
+ return match?.[1] ?? 0
+}
+
function normalizeAppNameForRanking(name: string): string {
return name
.replace(/\s+\((synced|legacy)\)\s*$/iu, '')
@@ -1225,6 +1283,11 @@ function filterComposioConnectors(rows: DirectoryComposioConnector[], query: str
}
function pluginPopularScore(plugin: DirectoryPluginSummary): number {
+ const pinnedRank = Math.max(
+ pinnedPopularRank(plugin.displayName, POPULAR_PLUGIN_TOP_20),
+ pinnedPopularRank(plugin.name, POPULAR_PLUGIN_TOP_20),
+ )
+ if (pinnedRank > 0) return 1_000_000 + pinnedRank
return (
(plugin.installed ? 500 : 0) +
(plugin.enabled ? 40 : 0) +
@@ -1237,6 +1300,8 @@ function pluginPopularScore(plugin: DirectoryPluginSummary): number {
function appPopularScore(app: DirectoryAppInfo): number {
const normalizedName = normalizeAppNameForRanking(app.name)
+ const pinnedRank = pinnedPopularRank(normalizedName, POPULAR_APP_TOP_20)
+ if (pinnedRank > 0) return 1_000_000 + pinnedRank
return (
(app.isAccessible ? 10_000 : 0) +
bonusForName(normalizedName, POPULAR_APP_NAME_BONUSES) +
diff --git a/src/components/content/directoryHubUtils.ts b/src/components/content/directoryHubUtils.ts
index c39369563..1817cea72 100644
--- a/src/components/content/directoryHubUtils.ts
+++ b/src/components/content/directoryHubUtils.ts
@@ -2,6 +2,28 @@ import type { DirectoryComposioConnector } from '../../api/codexGateway'
export type DirectorySortMode = 'popular' | 'name' | 'date'
+const POPULAR_COMPOSIO_TOP_20: Array<[RegExp, number]> = [
+ [/^gmail$/i, 20],
+ [/^google calendar$/i, 19],
+ [/^google drive$/i, 18],
+ [/^google docs$/i, 17],
+ [/^google sheets$/i, 16],
+ [/^slack$/i, 15],
+ [/^github$/i, 14],
+ [/^notion$/i, 13],
+ [/^linear$/i, 12],
+ [/^jira$/i, 11],
+ [/^outlook$/i, 10],
+ [/^(teams|microsoft teams)$/i, 9],
+ [/^dropbox$/i, 8],
+ [/^trello$/i, 7],
+ [/^asana$/i, 6],
+ [/^hubspot$/i, 5],
+ [/^salesforce$/i, 4],
+ [/^figma$/i, 3],
+ [/^youtube$/i, 2],
+ [/^(x|twitter)$/i, 1],
+]
const POPULAR_COMPOSIO_NAME_BONUSES: Array<[RegExp, number]> = [
[/(gmail|google calendar|google docs|google sheets|google drive|github|slack|notion|linear|outlook|supabase)/i, 140],
[/(email|calendar|document|sheet|drive|repo|issue|message|project|database|crm|deploy)/i, 50],
@@ -15,7 +37,28 @@ function bonusForName(name: string, rows: Array<[RegExp, number]>): number {
return rows.reduce((score, [pattern, bonus]) => score + (pattern.test(name) ? bonus : 0), 0)
}
+function normalizePopularRankName(name: string): string {
+ return name
+ .trim()
+ .replace(/\s+\((synced|legacy)\)\s*$/iu, '')
+ .replace(/\s+\(.*?\)\s*$/u, '')
+ .replace(/[-_]+/gu, ' ')
+ .replace(/\s+/gu, ' ')
+ .trim()
+}
+
+function pinnedPopularRank(connector: DirectoryComposioConnector): number {
+ const names = [connector.name, connector.slug].map(normalizePopularRankName)
+ for (const name of names) {
+ const match = POPULAR_COMPOSIO_TOP_20.find(([pattern]) => pattern.test(name))
+ if (match) return match[1]
+ }
+ return 0
+}
+
function composioPopularScore(connector: DirectoryComposioConnector): number {
+ const pinnedRank = pinnedPopularRank(connector)
+ if (pinnedRank > 0) return 1_000_000 + pinnedRank
return (
(connector.activeCount * 1_000) +
(connector.isNoAuth ? 300 : 0) +
@@ -65,6 +108,8 @@ export function sortComposioConnectors(
}
return [...rows].sort((a, b) => (
(queryRank(b) - queryRank(a)) ||
- (composioConnectionRank(a) - composioConnectionRank(b))
- ) || (composioPopularScore(b) - composioPopularScore(a)) || a.name.localeCompare(b.name))
+ (composioPopularScore(b) - composioPopularScore(a)) ||
+ (composioConnectionRank(a) - composioConnectionRank(b)) ||
+ a.name.localeCompare(b.name)
+ ))
}
diff --git a/tests.md b/tests.md
index e410c4c94..651591040 100644
--- a/tests.md
+++ b/tests.md
@@ -349,15 +349,20 @@ Plugin catalog popular examples.
#### Steps
1. In light theme, open `/#/skills?tab=plugins`.
2. Confirm plugin cards are sorted by `Popular` by default.
-3. Confirm each visible plugin card shows five concrete example chips, such as browser, GitHub, email, calendar, file, deploy, or fallback action examples, instead of only generic category/capability chips.
-4. Open an installed plugin detail and confirm the same five example chips are shown in the `Examples` section.
-5. Open a plugin that is not installed or is unavailable for install and confirm its example chips remain visible in the card and detail view.
-6. Search by text from an example chip and confirm matching plugins remain discoverable.
-7. Switch to dark theme and repeat steps 1-6.
+3. Confirm the first plugin cards follow the hardcoded casual-user popularity order when those plugins are available, then fall back to heuristic scoring for the rest.
+4. Confirm each visible plugin card shows five concrete example chips, such as browser, GitHub, email, calendar, file, deploy, or fallback action examples, instead of only generic category/capability chips.
+5. Open an installed plugin detail and confirm the same five example chips are shown in the `Examples` section.
+6. Open a plugin that is not installed or is unavailable for install and confirm its example chips remain visible in the card and detail view.
+7. Search by text from an example chip and confirm matching plugins remain discoverable.
+8. Open the Apps tab and confirm `Popular` uses the hardcoded casual-user app order for available top entries, then heuristic scoring for the rest.
+9. Open the Composio tab after login and confirm `Popular` uses the hardcoded casual-user connector order for available top entries, then heuristic scoring for the rest.
+10. Switch to dark theme and repeat steps 1-9.
#### Expected Results
- Plugin examples are visible before installation and are not gated by installed/enabled state.
- Popular plugin families show practical examples tailored to their domain.
+- Plugin, Apps, and Composio popular sorting put the hardcoded casual-user top 20 entries first when available.
+- Rows outside the hardcoded top 20 continue to use the existing heuristic popularity score.
- Plugins without recognized domains still show five fallback examples derived from default prompts, capabilities, or safe generic actions.
- Example chips remain readable in light theme and dark theme.
From 4887ce7ab2b91011f06536876b6d8e57e0630f15 Mon Sep 17 00:00:00 2001
From: Igor
Date: Wed, 13 May 2026 08:15:02 +0700
Subject: [PATCH 3/8] Preload directory catalogs in background
---
src/App.vue | 25 +++
src/api/codexGateway.ts | 203 +++++++++++++++++++-----
src/components/content/DirectoryHub.vue | 16 +-
tests.md | 6 +-
4 files changed, 198 insertions(+), 52 deletions(-)
diff --git a/src/App.vue b/src/App.vue
index 034797e70..2773c17ba 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1144,6 +1144,10 @@ import {
getThreadTerminalQuickCommands,
getThreadTerminalStatus,
getWorkspaceRootsState,
+ getDirectoryComposioStatus,
+ listDirectoryApps,
+ listDirectoryComposioConnectors,
+ listDirectoryPlugins,
listLocalDirectories,
openProjectRoot,
persistFirstLaunchPluginsCardPreference,
@@ -2010,6 +2014,9 @@ onMounted(() => {
void loadFreeModeStatus()
void refreshThreadTerminalStatus()
void refreshTerminalQuickCommands()
+ window.setTimeout(() => {
+ void preloadDirectoryCatalogs()
+ }, 1500)
})
watch(visibleFeedbackErrors, (values, oldValues) => {
@@ -4154,6 +4161,24 @@ function onSelectCollaborationMode(mode: 'default' | 'plan'): void {
setSelectedCollaborationMode(mode)
}
+async function preloadDirectoryCatalogs(): Promise {
+ const cwd = directoryCwd.value.trim()
+ const threadId = routeThreadId.value.trim()
+ try {
+ const composioStatusPromise = getDirectoryComposioStatus()
+ await Promise.allSettled([
+ listDirectoryPlugins(cwd ? [cwd] : undefined),
+ listDirectoryApps(threadId || undefined),
+ composioStatusPromise.then(async (status) => {
+ if (!status.available || !status.authenticated) return
+ await listDirectoryComposioConnectors('', null, 50)
+ }),
+ ])
+ } catch {
+ // Background preloading should never block normal navigation.
+ }
+}
+
async function initialize(): Promise {
await router.isReady()
diff --git a/src/api/codexGateway.ts b/src/api/codexGateway.ts
index c7bea2275..a61499d0c 100644
--- a/src/api/codexGateway.ts
+++ b/src/api/codexGateway.ts
@@ -2103,27 +2103,86 @@ function normalizeDirectoryMcpServer(value: unknown): DirectoryMcpServerStatus |
}
}
-export async function listDirectoryPlugins(cwds?: string[]): Promise {
+type DirectoryCacheOptions = { force?: boolean }
+
+const directoryPluginCache = new Map | null; value: DirectoryPluginSummary[] | null }>()
+const directoryAppCache = new Map | null; value: DirectoryAppInfo[] | null }>()
+const directoryComposioConnectorCache = new Map | null; value: DirectoryComposioConnectorPage | null }>()
+let directoryComposioStatusCache: { promise: Promise | null; value: DirectoryComposioStatus | null } = { promise: null, value: null }
+
+function directoryPluginsCacheKey(cwds?: string[]): string {
+ return (cwds ?? []).map((cwd) => cwd.trim()).filter(Boolean).join('\n')
+}
+
+function cloneDirectoryPluginRows(rows: DirectoryPluginSummary[]): DirectoryPluginSummary[] {
+ return rows.map((row) => ({ ...row, capabilities: [...row.capabilities], defaultPrompt: [...row.defaultPrompt], screenshotUrls: [...row.screenshotUrls], screenshots: [...row.screenshots] }))
+}
+
+function cloneDirectoryAppRows(rows: DirectoryAppInfo[]): DirectoryAppInfo[] {
+ return rows.map((row) => ({ ...row, pluginDisplayNames: [...row.pluginDisplayNames] }))
+}
+
+function cloneDirectoryComposioConnectorRows(rows: DirectoryComposioConnector[]): DirectoryComposioConnector[] {
+ return rows.map((row) => ({
+ ...row,
+ authModes: [...row.authModes],
+ connectionStatuses: [...row.connectionStatuses],
+ }))
+}
+
+function cloneDirectoryComposioConnectorPage(page: DirectoryComposioConnectorPage): DirectoryComposioConnectorPage {
+ return {
+ data: cloneDirectoryComposioConnectorRows(page.data),
+ nextCursor: page.nextCursor,
+ total: page.total,
+ }
+}
+
+function clearDirectoryCatalogCaches(): void {
+ directoryPluginCache.clear()
+ directoryAppCache.clear()
+ directoryComposioConnectorCache.clear()
+ directoryComposioStatusCache = { promise: null, value: null }
+}
+
+export async function listDirectoryPlugins(cwds?: string[], options: DirectoryCacheOptions = {}): Promise {
+ const cacheKey = directoryPluginsCacheKey(cwds)
+ const cached = directoryPluginCache.get(cacheKey)
+ if (!options.force && cached?.value) return cloneDirectoryPluginRows(cached.value)
+ if (!options.force && cached?.promise) return cloneDirectoryPluginRows(await cached.promise)
+
const params: Record = {}
if (cwds && cwds.length > 0) params.cwds = cwds
- 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 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 {
+ return cloneDirectoryPluginRows(await request)
+ } catch (error) {
+ if (directoryPluginCache.get(cacheKey)?.promise === request) {
+ directoryPluginCache.delete(cacheKey)
}
+ throw error
}
- return plugins
}
export async function readDirectoryPlugin(plugin: DirectoryPluginSummary): Promise {
@@ -2156,6 +2215,7 @@ export async function installDirectoryPlugin(plugin: DirectoryPluginSummary): Pr
if (plugin.marketplacePath) params.marketplacePath = plugin.marketplacePath
if (plugin.remoteMarketplaceName) params.remoteMarketplaceName = plugin.remoteMarketplaceName
const payload = await callRpc<{ authPolicy?: string; auth_policy?: string; appsNeedingAuth?: unknown[]; apps_needing_auth?: unknown[] }>('plugin/install', params)
+ clearDirectoryCatalogCaches()
const apps = payload.appsNeedingAuth ?? payload.apps_needing_auth ?? []
return {
authPolicy: readString(payload.authPolicy ?? payload.auth_policy) ?? '',
@@ -2165,6 +2225,7 @@ export async function installDirectoryPlugin(plugin: DirectoryPluginSummary): Pr
export async function uninstallDirectoryPlugin(pluginId: string): Promise {
await callRpc('plugin/uninstall', { pluginId })
+ clearDirectoryCatalogCaches()
}
export async function setDirectoryPluginEnabled(pluginId: string, enabled: boolean): Promise {
@@ -2174,25 +2235,43 @@ export async function setDirectoryPluginEnabled(pluginId: string, enabled: boole
expectedVersion: null,
reloadUserConfig: true,
})
-}
-
-export async function listDirectoryApps(threadId?: string): Promise {
- const apps: DirectoryAppInfo[] = []
- let cursor: string | null = null
- let catalogRank = 0
- do {
- const params: Record = { limit: 100 }
- if (cursor) params.cursor = cursor
- if (threadId) params.threadId = threadId
- const payload = await callRpc<{ data?: unknown[]; nextCursor?: string | null; next_cursor?: string | null }>('app/list', params)
- for (const item of payload.data ?? []) {
- const app = normalizeDirectoryApp(item, catalogRank)
- if (app) apps.push(app)
- catalogRank += 1
+ directoryPluginCache.clear()
+}
+
+export async function listDirectoryApps(threadId?: string, options: DirectoryCacheOptions = {}): Promise {
+ const cacheKey = threadId?.trim() ?? ''
+ const cached = directoryAppCache.get(cacheKey)
+ if (!options.force && cached?.value) return cloneDirectoryAppRows(cached.value)
+ if (!options.force && cached?.promise) return cloneDirectoryAppRows(await cached.promise)
+
+ const request = (async () => {
+ const apps: DirectoryAppInfo[] = []
+ let cursor: string | null = null
+ let catalogRank = 0
+ do {
+ const params: Record = { limit: 100 }
+ if (cursor) params.cursor = cursor
+ if (cacheKey) params.threadId = cacheKey
+ const payload = await callRpc<{ data?: unknown[]; nextCursor?: string | null; next_cursor?: string | null }>('app/list', params)
+ for (const item of payload.data ?? []) {
+ const app = normalizeDirectoryApp(item, catalogRank)
+ if (app) apps.push(app)
+ catalogRank += 1
+ }
+ cursor = readString(payload.nextCursor ?? payload.next_cursor)
+ } while (cursor)
+ directoryAppCache.set(cacheKey, { promise: null, value: cloneDirectoryAppRows(apps) })
+ return apps
+ })()
+ directoryAppCache.set(cacheKey, { promise: request, value: options.force ? null : cached?.value ?? null })
+ try {
+ return cloneDirectoryAppRows(await request)
+ } catch (error) {
+ if (directoryAppCache.get(cacheKey)?.promise === request) {
+ directoryAppCache.delete(cacheKey)
}
- cursor = readString(payload.nextCursor ?? payload.next_cursor)
- } while (cursor)
- return apps
+ throw error
+ }
}
export async function setDirectoryAppEnabled(appId: string, enabled: boolean): Promise {
@@ -2202,6 +2281,7 @@ export async function setDirectoryAppEnabled(appId: string, enabled: boolean): P
expectedVersion: null,
reloadUserConfig: true,
})
+ directoryAppCache.clear()
}
export async function listDirectoryMcpServers(): Promise {
@@ -2231,34 +2311,71 @@ export async function startDirectoryMcpLogin(name: string): Promise {
- const response = await fetch('/codex-api/composio/status')
- if (!response.ok) {
- throw new Error(`Failed to load Composio status (${response.status})`)
+export async function getDirectoryComposioStatus(options: DirectoryCacheOptions = {}): Promise {
+ if (!options.force && directoryComposioStatusCache.value) return { ...directoryComposioStatusCache.value }
+ if (!options.force && directoryComposioStatusCache.promise) return { ...await directoryComposioStatusCache.promise }
+ const request = (async () => {
+ const response = await fetch('/codex-api/composio/status')
+ if (!response.ok) {
+ throw new Error(`Failed to load Composio status (${response.status})`)
+ }
+ const status = await response.json() as DirectoryComposioStatus
+ directoryComposioStatusCache = { promise: null, value: { ...status } }
+ return status
+ })()
+ directoryComposioStatusCache = { promise: request, value: options.force ? null : directoryComposioStatusCache.value }
+ try {
+ return { ...await request }
+ } catch (error) {
+ if (directoryComposioStatusCache.promise === request) {
+ directoryComposioStatusCache = { promise: null, value: null }
+ }
+ throw error
}
- return await response.json() as DirectoryComposioStatus
}
export async function listDirectoryComposioConnectors(
query = '',
cursor: string | null = null,
limit = 50,
+ options: DirectoryCacheOptions = {},
): Promise {
+ const normalizedQuery = query.trim()
+ const normalizedCursor = cursor?.trim() || ''
+ const normalizedLimit = limit && Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 50
+ const cacheKey = `${normalizedQuery}\n${normalizedCursor}\n${normalizedLimit}`
+ const cached = directoryComposioConnectorCache.get(cacheKey)
+ if (!options.force && cached?.value) return cloneDirectoryComposioConnectorPage(cached.value)
+ if (!options.force && cached?.promise) return cloneDirectoryComposioConnectorPage(await cached.promise)
+
+ const request = (async () => {
const params = new URLSearchParams()
- if (query.trim()) params.set('query', query.trim())
- if (cursor) params.set('cursor', cursor)
- if (limit && Number.isFinite(limit)) params.set('limit', String(Math.max(1, Math.floor(limit))))
+ if (normalizedQuery) params.set('query', normalizedQuery)
+ if (normalizedCursor) params.set('cursor', normalizedCursor)
+ params.set('limit', String(normalizedLimit))
const suffix = params.toString()
const response = await fetch(`/codex-api/composio/connectors${suffix ? `?${suffix}` : ''}`)
if (!response.ok) {
throw new Error(`Failed to list Composio connectors (${response.status})`)
}
const payload = await response.json() as DirectoryComposioConnectorPage | { data?: DirectoryComposioConnector[]; nextCursor?: string | null; total?: number }
- return {
+ const page = {
data: Array.isArray(payload.data) ? payload.data : [],
nextCursor: typeof payload.nextCursor === 'string' && payload.nextCursor.length > 0 ? payload.nextCursor : null,
total: typeof payload.total === 'number' && Number.isFinite(payload.total) ? Math.max(0, Math.floor(payload.total)) : 0,
}
+ directoryComposioConnectorCache.set(cacheKey, { promise: null, value: cloneDirectoryComposioConnectorPage(page) })
+ return page
+ })()
+ directoryComposioConnectorCache.set(cacheKey, { promise: request, value: options.force ? null : cached?.value ?? null })
+ try {
+ return cloneDirectoryComposioConnectorPage(await request)
+ } catch (error) {
+ if (directoryComposioConnectorCache.get(cacheKey)?.promise === request) {
+ directoryComposioConnectorCache.delete(cacheKey)
+ }
+ throw error
+ }
}
export async function readDirectoryComposioConnector(slug: string): Promise {
diff --git a/src/components/content/DirectoryHub.vue b/src/components/content/DirectoryHub.vue
index cde866dc7..b1066f0ec 100644
--- a/src/components/content/DirectoryHub.vue
+++ b/src/components/content/DirectoryHub.vue
@@ -1492,7 +1492,7 @@ async function loadPlugins(force = false): Promise {
pluginError.value = ''
try {
const [nextPlugins] = await Promise.all([
- listDirectoryPlugins(key ? [key] : undefined),
+ listDirectoryPlugins(key ? [key] : undefined, { force }),
supportsApps.value ? loadApps(force) : Promise.resolve(),
])
plugins.value = nextPlugins
@@ -1516,7 +1516,7 @@ async function loadApps(force = false): Promise {
isLoadingApps.value = true
appError.value = ''
try {
- apps.value = await listDirectoryApps(key || undefined)
+ apps.value = await listDirectoryApps(key || undefined, { force })
appsLoadedKey = key
} catch (error) {
appError.value = error instanceof Error ? error.message : 'Failed to load apps'
@@ -1528,7 +1528,7 @@ async function loadApps(force = false): Promise {
return appsLoadPromise
}
-async function loadComposio(append = false): Promise {
+async function loadComposio(append = false, force = false): Promise {
if (isLoadingComposio.value) {
isComposioLoadQueued = true
return
@@ -1537,7 +1537,7 @@ async function loadComposio(append = false): Promise {
isLoadingComposio.value = true
composioError.value = ''
try {
- const status = await getDirectoryComposioStatus()
+ const status = await getDirectoryComposioStatus({ force })
composioStatus.value = status
if (!status.available || !status.authenticated) {
composioConnectors.value = []
@@ -1546,7 +1546,7 @@ async function loadComposio(append = false): Promise {
return
}
const cursor = append ? composioNextCursor.value : null
- const page = await listDirectoryComposioConnectors(composioSearchQuery.value, cursor, COMPOSIO_PAGE_LIMIT)
+ const page = await listDirectoryComposioConnectors(composioSearchQuery.value, cursor, COMPOSIO_PAGE_LIMIT, { force })
composioConnectors.value = append ? [...composioConnectors.value, ...page.data] : page.data
composioNextCursor.value = page.nextCursor
composioTotal.value = page.total
@@ -1594,7 +1594,7 @@ async function refreshMcpStatusesForPluginDetail(): Promise {
function refreshActiveTab(forceReload = false): void {
if (activeTab.value === 'plugins') void loadPlugins(forceReload)
if (activeTab.value === 'apps') void loadApps(forceReload)
- if (activeTab.value === 'composio') void loadComposio()
+ if (activeTab.value === 'composio') void loadComposio(false, forceReload)
if (activeTab.value === 'skills') {
if (forceReload && supportsMcpReload.value) void reloadMcps()
else void loadMcps()
@@ -1606,7 +1606,7 @@ async function manualRefreshActiveTab(): Promise {
try {
if (activeTab.value === 'plugins') await loadPlugins(true)
else if (activeTab.value === 'apps') await loadApps(true)
- else if (activeTab.value === 'composio') await loadComposio()
+ else if (activeTab.value === 'composio') await loadComposio(false, true)
else if (activeTab.value === 'skills' && supportsMcpReload.value) await reloadMcps()
else if (activeTab.value === 'skills') await loadMcps()
} finally {
@@ -1692,7 +1692,7 @@ async function startComposioConnect(connector: DirectoryComposioConnector): Prom
}
openExternalUrl(result.redirectUrl)
showToast(`Opened ${connector.name} authorization`)
- await loadComposio()
+ await loadComposio(false, true)
if (isComposioDetailOpen.value && selectedComposioDetail.value?.connector.slug === connector.slug) {
await openComposioDetail(connector.slug)
}
diff --git a/tests.md b/tests.md
index 651591040..9a96475ba 100644
--- a/tests.md
+++ b/tests.md
@@ -356,13 +356,17 @@ Plugin catalog popular examples.
7. Search by text from an example chip and confirm matching plugins remain discoverable.
8. Open the Apps tab and confirm `Popular` uses the hardcoded casual-user app order for available top entries, then heuristic scoring for the rest.
9. Open the Composio tab after login and confirm `Popular` uses the hardcoded casual-user connector order for available top entries, then heuristic scoring for the rest.
-10. Switch to dark theme and repeat steps 1-9.
+10. Reload the app on a non-Skills route, wait two seconds, then open the Skills route and confirm Plugins, Apps, and the first Composio page use preloaded catalog data instead of showing a long initial loading pause.
+11. Use Refresh or perform a plugin install/uninstall/enable/disable action and confirm the catalog fetches fresh data instead of staying pinned to the preloaded cache.
+12. Switch to dark theme and repeat steps 1-11.
#### Expected Results
- Plugin examples are visible before installation and are not gated by installed/enabled state.
- Popular plugin families show practical examples tailored to their domain.
- Plugin, Apps, and Composio popular sorting put the hardcoded casual-user top 20 entries first when available.
- Rows outside the hardcoded top 20 continue to use the existing heuristic popularity score.
+- Plugin, Apps, and Composio catalog data is preloaded in the background and reused when opening the Skills route.
+- Manual refresh and plugin mutation flows bypass the preload cache and update from backend truth.
- Plugins without recognized domains still show five fallback examples derived from default prompts, capabilities, or safe generic actions.
- Example chips remain readable in light theme and dark theme.
From 31c1f6aa49ec034ddfd7f2653081e085e896d490 Mon Sep 17 00:00:00 2001
From: Igor
Date: Wed, 13 May 2026 08:16:33 +0700
Subject: [PATCH 4/8] Tune casual directory popularity ranking
---
src/components/content/DirectoryHub.vue | 76 ++++++++++-----------
src/components/content/directoryHubUtils.ts | 34 ++++-----
tests.md | 8 +--
3 files changed, 59 insertions(+), 59 deletions(-)
diff --git a/src/components/content/DirectoryHub.vue b/src/components/content/DirectoryHub.vue
index b1066f0ec..2b348fb4d 100644
--- a/src/components/content/DirectoryHub.vue
+++ b/src/components/content/DirectoryHub.vue
@@ -703,48 +703,48 @@ const COMPOSIO_PAGE_LIMIT = 50
const POPULAR_LIMIT = 100
const POPULAR_PLUGIN_TOP_20: Array<[RegExp, number]> = [
- [/^(browser|browser use|in app browser)$/i, 20],
- [/^chrome$/i, 19],
- [/^(computer use|desktop)$/i, 18],
- [/^github$/i, 17],
- [/^gmail$/i, 16],
- [/^google calendar$/i, 15],
- [/^outlook email$/i, 14],
- [/^google drive$/i, 13],
- [/^slack$/i, 12],
- [/^notion$/i, 11],
- [/^figma$/i, 10],
- [/^canva$/i, 9],
- [/^linear$/i, 8],
- [/^jira$/i, 7],
- [/^google docs$/i, 6],
- [/^google sheets$/i, 5],
- [/^dropbox$/i, 4],
- [/^(teams|microsoft teams)$/i, 3],
- [/^sharepoint$/i, 2],
- [/^cloudflare$/i, 1],
+ [/^gmail$/i, 20],
+ [/^google calendar$/i, 19],
+ [/^google drive$/i, 18],
+ [/^reddit$/i, 17],
+ [/^(x|twitter)$/i, 16],
+ [/^youtube$/i, 15],
+ [/^instagram$/i, 14],
+ [/^tiktok$/i, 13],
+ [/^facebook$/i, 12],
+ [/^linkedin$/i, 11],
+ [/^slack$/i, 10],
+ [/^notion$/i, 9],
+ [/^canva$/i, 8],
+ [/^figma$/i, 7],
+ [/^dropbox$/i, 6],
+ [/^(teams|microsoft teams)$/i, 5],
+ [/^outlook email$/i, 4],
+ [/^outlook calendar$/i, 3],
+ [/^github$/i, 2],
+ [/^spotify$/i, 1],
]
const POPULAR_APP_TOP_20: Array<[RegExp, number]> = [
[/^gmail$/i, 20],
[/^google calendar$/i, 19],
- [/^outlook email$/i, 18],
- [/^outlook calendar$/i, 17],
- [/^google drive$/i, 16],
- [/^google docs$/i, 15],
- [/^google sheets$/i, 14],
- [/^dropbox$/i, 13],
- [/^slack$/i, 12],
- [/^notion$/i, 11],
- [/^canva$/i, 10],
- [/^figma$/i, 9],
- [/^github$/i, 8],
- [/^linear$/i, 7],
- [/^jira$/i, 6],
- [/^trello$/i, 5],
- [/^asana$/i, 4],
- [/^youtube$/i, 3],
- [/^(x|twitter)$/i, 2],
- [/^salesforce$/i, 1],
+ [/^google drive$/i, 18],
+ [/^reddit$/i, 17],
+ [/^(x|twitter)$/i, 16],
+ [/^youtube$/i, 15],
+ [/^instagram$/i, 14],
+ [/^tiktok$/i, 13],
+ [/^facebook$/i, 12],
+ [/^linkedin$/i, 11],
+ [/^slack$/i, 10],
+ [/^notion$/i, 9],
+ [/^canva$/i, 8],
+ [/^figma$/i, 7],
+ [/^dropbox$/i, 6],
+ [/^outlook email$/i, 5],
+ [/^outlook calendar$/i, 4],
+ [/^google docs$/i, 3],
+ [/^google sheets$/i, 2],
+ [/^spotify$/i, 1],
]
const POPULAR_APP_NAME_BONUSES: Array<[RegExp, number]> = [
[/^gmail$/i, 30_000],
diff --git a/src/components/content/directoryHubUtils.ts b/src/components/content/directoryHubUtils.ts
index 1817cea72..007975619 100644
--- a/src/components/content/directoryHubUtils.ts
+++ b/src/components/content/directoryHubUtils.ts
@@ -6,23 +6,23 @@ const POPULAR_COMPOSIO_TOP_20: Array<[RegExp, number]> = [
[/^gmail$/i, 20],
[/^google calendar$/i, 19],
[/^google drive$/i, 18],
- [/^google docs$/i, 17],
- [/^google sheets$/i, 16],
- [/^slack$/i, 15],
- [/^github$/i, 14],
- [/^notion$/i, 13],
- [/^linear$/i, 12],
- [/^jira$/i, 11],
- [/^outlook$/i, 10],
- [/^(teams|microsoft teams)$/i, 9],
- [/^dropbox$/i, 8],
- [/^trello$/i, 7],
- [/^asana$/i, 6],
- [/^hubspot$/i, 5],
- [/^salesforce$/i, 4],
- [/^figma$/i, 3],
- [/^youtube$/i, 2],
- [/^(x|twitter)$/i, 1],
+ [/^reddit$/i, 17],
+ [/^(x|twitter)$/i, 16],
+ [/^youtube$/i, 15],
+ [/^instagram$/i, 14],
+ [/^tiktok$/i, 13],
+ [/^facebook$/i, 12],
+ [/^linkedin$/i, 11],
+ [/^slack$/i, 10],
+ [/^notion$/i, 9],
+ [/^canva$/i, 8],
+ [/^figma$/i, 7],
+ [/^dropbox$/i, 6],
+ [/^outlook$/i, 5],
+ [/^(teams|microsoft teams)$/i, 4],
+ [/^google docs$/i, 3],
+ [/^google sheets$/i, 2],
+ [/^spotify$/i, 1],
]
const POPULAR_COMPOSIO_NAME_BONUSES: Array<[RegExp, number]> = [
[/(gmail|google calendar|google docs|google sheets|google drive|github|slack|notion|linear|outlook|supabase)/i, 140],
diff --git a/tests.md b/tests.md
index 9a96475ba..0fbcb818b 100644
--- a/tests.md
+++ b/tests.md
@@ -349,13 +349,13 @@ Plugin catalog popular examples.
#### Steps
1. In light theme, open `/#/skills?tab=plugins`.
2. Confirm plugin cards are sorted by `Popular` by default.
-3. Confirm the first plugin cards follow the hardcoded casual-user popularity order when those plugins are available, then fall back to heuristic scoring for the rest.
+3. Confirm the first plugin cards follow the hardcoded casual-user popularity order when those plugins are available, prioritizing email, calendar, drive, and social apps such as Reddit, X/Twitter, YouTube, Instagram, TikTok, Facebook, and LinkedIn before falling back to heuristic scoring.
4. Confirm each visible plugin card shows five concrete example chips, such as browser, GitHub, email, calendar, file, deploy, or fallback action examples, instead of only generic category/capability chips.
5. Open an installed plugin detail and confirm the same five example chips are shown in the `Examples` section.
6. Open a plugin that is not installed or is unavailable for install and confirm its example chips remain visible in the card and detail view.
7. Search by text from an example chip and confirm matching plugins remain discoverable.
-8. Open the Apps tab and confirm `Popular` uses the hardcoded casual-user app order for available top entries, then heuristic scoring for the rest.
-9. Open the Composio tab after login and confirm `Popular` uses the hardcoded casual-user connector order for available top entries, then heuristic scoring for the rest.
+8. Open the Apps tab and confirm `Popular` uses the hardcoded casual-user app order for available top entries, including social apps, then heuristic scoring for the rest.
+9. Open the Composio tab after login and confirm `Popular` uses the hardcoded casual-user connector order for available top entries, including social connectors, then heuristic scoring for the rest.
10. Reload the app on a non-Skills route, wait two seconds, then open the Skills route and confirm Plugins, Apps, and the first Composio page use preloaded catalog data instead of showing a long initial loading pause.
11. Use Refresh or perform a plugin install/uninstall/enable/disable action and confirm the catalog fetches fresh data instead of staying pinned to the preloaded cache.
12. Switch to dark theme and repeat steps 1-11.
@@ -363,7 +363,7 @@ Plugin catalog popular examples.
#### Expected Results
- Plugin examples are visible before installation and are not gated by installed/enabled state.
- Popular plugin families show practical examples tailored to their domain.
-- Plugin, Apps, and Composio popular sorting put the hardcoded casual-user top 20 entries first when available.
+- Plugin, Apps, and Composio popular sorting put the hardcoded casual-user top 20 entries first when available, with browser, Chrome, and computer-use entries left to heuristic sorting instead of pinned ranking.
- Rows outside the hardcoded top 20 continue to use the existing heuristic popularity score.
- Plugin, Apps, and Composio catalog data is preloaded in the background and reused when opening the Skills route.
- Manual refresh and plugin mutation flows bypass the preload cache and update from backend truth.
From cec4c2d7b3e7d51478359f9ea681ff5bdeacdbfe Mon Sep 17 00:00:00 2001
From: Igor
Date: Wed, 13 May 2026 08:19:54 +0700
Subject: [PATCH 5/8] Prioritize docs apps in directory popularity
---
src/components/content/DirectoryHub.vue | 64 ++++++++++-----------
src/components/content/directoryHubUtils.ts | 34 +++++------
tests.md | 2 +-
3 files changed, 50 insertions(+), 50 deletions(-)
diff --git a/src/components/content/DirectoryHub.vue b/src/components/content/DirectoryHub.vue
index 2b348fb4d..e953e2056 100644
--- a/src/components/content/DirectoryHub.vue
+++ b/src/components/content/DirectoryHub.vue
@@ -706,44 +706,44 @@ const POPULAR_PLUGIN_TOP_20: Array<[RegExp, number]> = [
[/^gmail$/i, 20],
[/^google calendar$/i, 19],
[/^google drive$/i, 18],
- [/^reddit$/i, 17],
- [/^(x|twitter)$/i, 16],
- [/^youtube$/i, 15],
- [/^instagram$/i, 14],
- [/^tiktok$/i, 13],
- [/^facebook$/i, 12],
- [/^linkedin$/i, 11],
- [/^slack$/i, 10],
- [/^notion$/i, 9],
- [/^canva$/i, 8],
- [/^figma$/i, 7],
- [/^dropbox$/i, 6],
- [/^(teams|microsoft teams)$/i, 5],
- [/^outlook email$/i, 4],
- [/^outlook calendar$/i, 3],
- [/^github$/i, 2],
+ [/^google docs$/i, 17],
+ [/^google sheets$/i, 16],
+ [/^notion$/i, 15],
+ [/^obsidian$/i, 14],
+ [/^reddit$/i, 13],
+ [/^(x|twitter)$/i, 12],
+ [/^youtube$/i, 11],
+ [/^instagram$/i, 10],
+ [/^tiktok$/i, 9],
+ [/^facebook$/i, 8],
+ [/^linkedin$/i, 7],
+ [/^slack$/i, 6],
+ [/^canva$/i, 5],
+ [/^figma$/i, 4],
+ [/^dropbox$/i, 3],
+ [/^outlook email$/i, 2],
[/^spotify$/i, 1],
]
const POPULAR_APP_TOP_20: Array<[RegExp, number]> = [
[/^gmail$/i, 20],
[/^google calendar$/i, 19],
[/^google drive$/i, 18],
- [/^reddit$/i, 17],
- [/^(x|twitter)$/i, 16],
- [/^youtube$/i, 15],
- [/^instagram$/i, 14],
- [/^tiktok$/i, 13],
- [/^facebook$/i, 12],
- [/^linkedin$/i, 11],
- [/^slack$/i, 10],
- [/^notion$/i, 9],
- [/^canva$/i, 8],
- [/^figma$/i, 7],
- [/^dropbox$/i, 6],
- [/^outlook email$/i, 5],
- [/^outlook calendar$/i, 4],
- [/^google docs$/i, 3],
- [/^google sheets$/i, 2],
+ [/^google docs$/i, 17],
+ [/^google sheets$/i, 16],
+ [/^notion$/i, 15],
+ [/^obsidian$/i, 14],
+ [/^reddit$/i, 13],
+ [/^(x|twitter)$/i, 12],
+ [/^youtube$/i, 11],
+ [/^instagram$/i, 10],
+ [/^tiktok$/i, 9],
+ [/^facebook$/i, 8],
+ [/^linkedin$/i, 7],
+ [/^slack$/i, 6],
+ [/^canva$/i, 5],
+ [/^figma$/i, 4],
+ [/^dropbox$/i, 3],
+ [/^outlook email$/i, 2],
[/^spotify$/i, 1],
]
const POPULAR_APP_NAME_BONUSES: Array<[RegExp, number]> = [
diff --git a/src/components/content/directoryHubUtils.ts b/src/components/content/directoryHubUtils.ts
index 007975619..30223d285 100644
--- a/src/components/content/directoryHubUtils.ts
+++ b/src/components/content/directoryHubUtils.ts
@@ -6,26 +6,26 @@ const POPULAR_COMPOSIO_TOP_20: Array<[RegExp, number]> = [
[/^gmail$/i, 20],
[/^google calendar$/i, 19],
[/^google drive$/i, 18],
- [/^reddit$/i, 17],
- [/^(x|twitter)$/i, 16],
- [/^youtube$/i, 15],
- [/^instagram$/i, 14],
- [/^tiktok$/i, 13],
- [/^facebook$/i, 12],
- [/^linkedin$/i, 11],
- [/^slack$/i, 10],
- [/^notion$/i, 9],
- [/^canva$/i, 8],
- [/^figma$/i, 7],
- [/^dropbox$/i, 6],
- [/^outlook$/i, 5],
- [/^(teams|microsoft teams)$/i, 4],
- [/^google docs$/i, 3],
- [/^google sheets$/i, 2],
+ [/^google docs$/i, 17],
+ [/^google sheets$/i, 16],
+ [/^notion$/i, 15],
+ [/^obsidian$/i, 14],
+ [/^reddit$/i, 13],
+ [/^(x|twitter)$/i, 12],
+ [/^youtube$/i, 11],
+ [/^instagram$/i, 10],
+ [/^tiktok$/i, 9],
+ [/^facebook$/i, 8],
+ [/^linkedin$/i, 7],
+ [/^slack$/i, 6],
+ [/^canva$/i, 5],
+ [/^figma$/i, 4],
+ [/^dropbox$/i, 3],
+ [/^outlook$/i, 2],
[/^spotify$/i, 1],
]
const POPULAR_COMPOSIO_NAME_BONUSES: Array<[RegExp, number]> = [
- [/(gmail|google calendar|google docs|google sheets|google drive|github|slack|notion|linear|outlook|supabase)/i, 140],
+ [/(gmail|google calendar|google docs|google sheets|google drive|obsidian|github|slack|notion|linear|outlook|supabase)/i, 140],
[/(email|calendar|document|sheet|drive|repo|issue|message|project|database|crm|deploy)/i, 50],
]
diff --git a/tests.md b/tests.md
index 0fbcb818b..ce19cdf21 100644
--- a/tests.md
+++ b/tests.md
@@ -349,7 +349,7 @@ Plugin catalog popular examples.
#### Steps
1. In light theme, open `/#/skills?tab=plugins`.
2. Confirm plugin cards are sorted by `Popular` by default.
-3. Confirm the first plugin cards follow the hardcoded casual-user popularity order when those plugins are available, prioritizing email, calendar, drive, and social apps such as Reddit, X/Twitter, YouTube, Instagram, TikTok, Facebook, and LinkedIn before falling back to heuristic scoring.
+3. Confirm the first plugin cards follow the hardcoded casual-user popularity order when those plugins are available, prioritizing email, calendar, Drive, Docs, Sheets, Notion, Obsidian, and social apps such as Reddit, X/Twitter, YouTube, Instagram, TikTok, Facebook, and LinkedIn before falling back to heuristic scoring.
4. Confirm each visible plugin card shows five concrete example chips, such as browser, GitHub, email, calendar, file, deploy, or fallback action examples, instead of only generic category/capability chips.
5. Open an installed plugin detail and confirm the same five example chips are shown in the `Examples` section.
6. Open a plugin that is not installed or is unavailable for install and confirm its example chips remain visible in the card and detail view.
From 106a3044a190a2fbdb4ba6f38f6e8b8d53d35eb4 Mon Sep 17 00:00:00 2001
From: Igor
Date: Wed, 13 May 2026 08:22:41 +0700
Subject: [PATCH 6/8] Add creative directory example chips
---
src/components/content/DirectoryHub.vue | 115 +++++++++++++++++-------
tests.md | 7 +-
2 files changed, 86 insertions(+), 36 deletions(-)
diff --git a/src/components/content/DirectoryHub.vue b/src/components/content/DirectoryHub.vue
index e953e2056..3abfb8618 100644
--- a/src/components/content/DirectoryHub.vue
+++ b/src/components/content/DirectoryHub.vue
@@ -165,9 +165,8 @@
{{ app.description }}
-
-
{{ app.category }}
-
{{ name }}
+
+ {{ example }}
@@ -323,10 +322,8 @@
{{ connector.description }}
-
-
{{ connector.toolsCount }} tools
-
{{ connector.triggersCount }} triggers
-
{{ connector.authModes.join(', ') }}
+
+ {{ example }}
@@ -787,6 +784,46 @@ const POPULAR_PLUGIN_NAME_BONUSES: Array<[RegExp, number]> = [
[/(web|docs|design|deploy|project|issue|search|database|outlook|teams|sharepoint|jira|vercel|netlify)/i, 75],
]
const PLUGIN_EXAMPLE_RULES: Array<{ pattern: RegExp; examples: string[] }> = [
+ {
+ pattern: /(gmail|email|outlook|mail|inbox)/i,
+ examples: ['Triage inbox chaos', 'Extract hidden todos', 'Draft the hard reply', 'Find attachments fast', 'Clean newsletter pile'],
+ },
+ {
+ pattern: /(calendar|event|availability|meeting)/i,
+ examples: ['Rescue my week', 'Find focus blocks', 'Prep meeting brief', 'Move conflicts cleanly', 'Audit time leaks'],
+ },
+ {
+ pattern: /(google drive|drive|dropbox|box|sharepoint|file|storage)/i,
+ examples: ['Find the source doc', 'Brief from scattered files', 'Compare recent edits', 'Summarize a folder', 'Pull proof for a claim'],
+ },
+ {
+ pattern: /(google docs|docs|document)/i,
+ examples: ['Turn notes into a doc', 'Draft a first pass', 'Extract decisions', 'Rewrite for clarity', 'Make an exec brief'],
+ },
+ {
+ pattern: /(google sheets|sheets|spreadsheet|excel)/i,
+ examples: ['Clean messy rows', 'Spot weird outliers', 'Make a quick pivot', 'Write formulas', 'Summarize the numbers'],
+ },
+ {
+ pattern: /(notion|wiki|page)/i,
+ examples: ['Update team wiki', 'Turn chaos into tasks', 'Find stale docs', 'Build a project brief', 'Extract action items'],
+ },
+ {
+ pattern: /(obsidian|vault|second brain|pkm|notes)/i,
+ examples: ['Search my vault', 'Connect related notes', 'Make a daily brief', 'Extract evergreen ideas', 'Turn notes into tasks'],
+ },
+ {
+ pattern: /(reddit|subreddit)/i,
+ examples: ['Find real user pain', 'Mine subreddit questions', 'Spot complaint patterns', 'Draft a helpful reply', 'Track niche trends'],
+ },
+ {
+ pattern: /(x|twitter)/i,
+ examples: ['Find trend angles', 'Draft a sharp thread', 'Monitor brand mentions', 'Turn post into replies', 'Compare hot takes'],
+ },
+ {
+ pattern: /(youtube|tiktok|instagram|facebook|linkedin|shorts|reels)/i,
+ examples: ['Pull hooks from comments', 'Repurpose into posts', 'Draft platform captions', 'Find creator angles', 'Plan content batch'],
+ },
{
pattern: /(browser|web)/i,
examples: ['Open a local URL', 'Click through a flow', 'Capture a screenshot', 'Inspect visible text', 'Verify mobile layout'],
@@ -811,22 +848,6 @@ const PLUGIN_EXAMPLE_RULES: Array<{ pattern: RegExp; examples: string[] }> = [
pattern: /(slack|teams|chat|message)/i,
examples: ['Summarize a channel', 'Find mentions', 'Draft a reply', 'Search a thread', 'Post an update'],
},
- {
- pattern: /(notion|docs|document|wiki|page)/i,
- examples: ['Find a page', 'Summarize docs', 'Create meeting notes', 'Update a wiki page', 'Extract action items'],
- },
- {
- pattern: /(gmail|email|outlook|mail|inbox)/i,
- examples: ['Summarize unread mail', 'Find an email', 'Draft a reply', 'Check attachments', 'Archive newsletters'],
- },
- {
- pattern: /(calendar|event|availability|meeting)/i,
- examples: ['Find free time', 'Summarize today', 'Create a meeting', 'Move an event', 'Check attendee status'],
- },
- {
- pattern: /(drive|dropbox|box|sharepoint|file|storage)/i,
- examples: ['Find a file', 'Read a document', 'Summarize a folder', 'Check recent edits', 'Share a file'],
- },
{
pattern: /(figma|canva|design|image|slide|presentation)/i,
examples: ['Inspect a design', 'Create a mockup', 'Export assets', 'Compare variants', 'Draft presentation copy'],
@@ -1131,10 +1152,9 @@ function pluginExampleSource(plugin: DirectoryPluginSummary): string {
].join(' ')
}
-function pluginExampleChips(plugin: DirectoryPluginSummary): string[] {
+function exampleChipsFromSource(source: string, fallbackLabel: string, fallbackValues: string[] = []): string[] {
const chips: string[] = []
const seen = new Set()
- const source = pluginExampleSource(plugin)
for (const rule of PLUGIN_EXAMPLE_RULES) {
if (!rule.pattern.test(source)) continue
@@ -1142,17 +1162,12 @@ function pluginExampleChips(plugin: DirectoryPluginSummary): string[] {
if (chips.length >= 5) return chips.slice(0, 5)
}
- for (const prompt of plugin.defaultPrompt) {
- addExampleChip(chips, seen, prompt.replace(/[.!?]\s*$/u, ''))
+ for (const value of fallbackValues) {
+ addExampleChip(chips, seen, value.replace(/[.!?]\s*$/u, ''))
if (chips.length >= 5) return chips.slice(0, 5)
}
- for (const capability of plugin.capabilities) {
- addExampleChip(chips, seen, `Try ${capability}`)
- if (chips.length >= 5) return chips.slice(0, 5)
- }
-
- addExampleChip(chips, seen, `Explore ${plugin.displayName}`)
+ addExampleChip(chips, seen, `Explore ${fallbackLabel}`)
addExampleChip(chips, seen, 'Show available actions')
addExampleChip(chips, seen, 'Run a safe check')
addExampleChip(chips, seen, 'Summarize current state')
@@ -1160,6 +1175,38 @@ function pluginExampleChips(plugin: DirectoryPluginSummary): string[] {
return chips.slice(0, 5)
}
+function pluginExampleChips(plugin: DirectoryPluginSummary): string[] {
+ const chips = exampleChipsFromSource(pluginExampleSource(plugin), plugin.displayName, plugin.defaultPrompt)
+ if (chips.length >= 5) return chips
+ const seen = new Set(chips.map((chip) => chip.toLowerCase()))
+ for (const capability of plugin.capabilities) {
+ addExampleChip(chips, seen, `Try ${capability}`)
+ if (chips.length >= 5) return chips.slice(0, 5)
+ }
+ return chips.slice(0, 5)
+}
+
+function appExampleChips(app: DirectoryAppInfo): string[] {
+ return exampleChipsFromSource([
+ app.name,
+ app.description,
+ app.category,
+ app.developer,
+ app.distributionChannel,
+ app.pluginDisplayNames.join(' '),
+ ].join(' '), app.name)
+}
+
+function composioExampleChips(connector: DirectoryComposioConnector): string[] {
+ return exampleChipsFromSource([
+ connector.name,
+ connector.slug,
+ connector.description,
+ connector.authModes.join(' '),
+ connector.connectionStatuses.join(' '),
+ ].join(' '), connector.name)
+}
+
function bonusForName(name: string, rows: Array<[RegExp, number]>): number {
return rows.reduce((score, [pattern, bonus]) => score + (pattern.test(name) ? bonus : 0), 0)
}
@@ -1269,6 +1316,7 @@ function filterApps(rows: DirectoryAppInfo[], query: string): DirectoryAppInfo[]
app.category,
app.distributionChannel,
...app.pluginDisplayNames,
+ ...appExampleChips(app),
], query))
}
@@ -1279,6 +1327,7 @@ function filterComposioConnectors(rows: DirectoryComposioConnector[], query: str
connector.description,
...connector.authModes,
...connector.connectionStatuses,
+ ...composioExampleChips(connector),
], query))
}
diff --git a/tests.md b/tests.md
index ce19cdf21..0e8eb7983 100644
--- a/tests.md
+++ b/tests.md
@@ -350,12 +350,12 @@ Plugin catalog popular examples.
1. In light theme, open `/#/skills?tab=plugins`.
2. Confirm plugin cards are sorted by `Popular` by default.
3. Confirm the first plugin cards follow the hardcoded casual-user popularity order when those plugins are available, prioritizing email, calendar, Drive, Docs, Sheets, Notion, Obsidian, and social apps such as Reddit, X/Twitter, YouTube, Instagram, TikTok, Facebook, and LinkedIn before falling back to heuristic scoring.
-4. Confirm each visible plugin card shows five concrete example chips, such as browser, GitHub, email, calendar, file, deploy, or fallback action examples, instead of only generic category/capability chips.
+4. Confirm each visible plugin card shows five concrete example chips, such as inbox triage, focus-block planning, second-brain search, social listening, content repurposing, or fallback action examples, instead of only generic category/capability chips.
5. Open an installed plugin detail and confirm the same five example chips are shown in the `Examples` section.
6. Open a plugin that is not installed or is unavailable for install and confirm its example chips remain visible in the card and detail view.
7. Search by text from an example chip and confirm matching plugins remain discoverable.
-8. Open the Apps tab and confirm `Popular` uses the hardcoded casual-user app order for available top entries, including social apps, then heuristic scoring for the rest.
-9. Open the Composio tab after login and confirm `Popular` uses the hardcoded casual-user connector order for available top entries, including social connectors, then heuristic scoring for the rest.
+8. Open the Apps tab and confirm `Popular` uses the hardcoded casual-user app order for available top entries, including social apps, then heuristic scoring for the rest; verify app cards show the same five creative example-chip style instead of category/plugin-name chips.
+9. Open the Composio tab after login and confirm `Popular` uses the hardcoded casual-user connector order for available top entries, including social connectors, then heuristic scoring for the rest; verify connector cards show creative example chips instead of only tool/auth metadata chips.
10. Reload the app on a non-Skills route, wait two seconds, then open the Skills route and confirm Plugins, Apps, and the first Composio page use preloaded catalog data instead of showing a long initial loading pause.
11. Use Refresh or perform a plugin install/uninstall/enable/disable action and confirm the catalog fetches fresh data instead of staying pinned to the preloaded cache.
12. Switch to dark theme and repeat steps 1-11.
@@ -363,6 +363,7 @@ Plugin catalog popular examples.
#### Expected Results
- Plugin examples are visible before installation and are not gated by installed/enabled state.
- Popular plugin families show practical examples tailored to their domain.
+- Apps and Composio connector cards also show five domain-specific example chips for top everyday workflows.
- Plugin, Apps, and Composio popular sorting put the hardcoded casual-user top 20 entries first when available, with browser, Chrome, and computer-use entries left to heuristic sorting instead of pinned ranking.
- Rows outside the hardcoded top 20 continue to use the existing heuristic popularity score.
- Plugin, Apps, and Composio catalog data is preloaded in the background and reused when opening the Skills route.
From a33a551fab4342aa0c60ff8c89a4ac5df9ccc943 Mon Sep 17 00:00:00 2001
From: Igor
Date: Wed, 13 May 2026 08:28:02 +0700
Subject: [PATCH 7/8] Make directory example chips concrete
---
src/components/content/DirectoryHub.vue | 20 ++++++++++----------
tests.md | 2 +-
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/src/components/content/DirectoryHub.vue b/src/components/content/DirectoryHub.vue
index 3abfb8618..5d81cd7d1 100644
--- a/src/components/content/DirectoryHub.vue
+++ b/src/components/content/DirectoryHub.vue
@@ -786,43 +786,43 @@ const POPULAR_PLUGIN_NAME_BONUSES: Array<[RegExp, number]> = [
const PLUGIN_EXAMPLE_RULES: Array<{ pattern: RegExp; examples: string[] }> = [
{
pattern: /(gmail|email|outlook|mail|inbox)/i,
- examples: ['Triage inbox chaos', 'Extract hidden todos', 'Draft the hard reply', 'Find attachments fast', 'Clean newsletter pile'],
+ examples: ['Make a VIP reply queue', 'Find invoices due this week', 'Draft no-thanks politely', 'List flight email changes', 'Archive promo clutter'],
},
{
pattern: /(calendar|event|availability|meeting)/i,
- examples: ['Rescue my week', 'Find focus blocks', 'Prep meeting brief', 'Move conflicts cleanly', 'Audit time leaks'],
+ examples: ['Protect 3 focus blocks', 'Prep my 2pm meeting', 'Find lunch with Alex', 'Move double-booked calls', 'Show meetings without notes'],
},
{
pattern: /(google drive|drive|dropbox|box|sharepoint|file|storage)/i,
- examples: ['Find the source doc', 'Brief from scattered files', 'Compare recent edits', 'Summarize a folder', 'Pull proof for a claim'],
+ examples: ['Find Q4 budget files', 'Brief from client folder', 'Compare latest contract edits', 'List stale shared docs', 'Find deck with churn chart'],
},
{
pattern: /(google docs|docs|document)/i,
- examples: ['Turn notes into a doc', 'Draft a first pass', 'Extract decisions', 'Rewrite for clarity', 'Make an exec brief'],
+ examples: ['Turn notes into PRD', 'Make a one-page brief', 'Extract decisions table', 'Rewrite as customer email', 'Add crisp section titles'],
},
{
pattern: /(google sheets|sheets|spreadsheet|excel)/i,
- examples: ['Clean messy rows', 'Spot weird outliers', 'Make a quick pivot', 'Write formulas', 'Summarize the numbers'],
+ examples: ['Clean duplicate leads', 'Flag spend outliers', 'Build revenue pivot', 'Write margin formulas', 'Draft 3-line KPI readout'],
},
{
pattern: /(notion|wiki|page)/i,
- examples: ['Update team wiki', 'Turn chaos into tasks', 'Find stale docs', 'Build a project brief', 'Extract action items'],
+ examples: ['Make launch task board', 'Find stale onboarding pages', 'Turn meeting notes to tasks', 'Draft product spec page', 'Summarize open decisions'],
},
{
pattern: /(obsidian|vault|second brain|pkm|notes)/i,
- examples: ['Search my vault', 'Connect related notes', 'Make a daily brief', 'Extract evergreen ideas', 'Turn notes into tasks'],
+ examples: ['Roll up weekly daily notes', 'Link orphan notes to projects', 'Turn YouTube link into note', 'Extract task log entries', 'Create insight index'],
},
{
pattern: /(reddit|subreddit)/i,
- examples: ['Find real user pain', 'Mine subreddit questions', 'Spot complaint patterns', 'Draft a helpful reply', 'Track niche trends'],
+ examples: ['Find 10 SaaS complaints', 'Mine r/productivity questions', 'Spot pricing objections', 'Draft non-salesy reply', 'Track competitor pain posts'],
},
{
pattern: /(x|twitter)/i,
- examples: ['Find trend angles', 'Draft a sharp thread', 'Monitor brand mentions', 'Turn post into replies', 'Compare hot takes'],
+ examples: ['Find angry launch replies', 'Draft 8-tweet teardown', 'Monitor brand mentions today', 'Turn blog into thread', 'Compare influencer takes'],
},
{
pattern: /(youtube|tiktok|instagram|facebook|linkedin|shorts|reels)/i,
- examples: ['Pull hooks from comments', 'Repurpose into posts', 'Draft platform captions', 'Find creator angles', 'Plan content batch'],
+ examples: ['Turn video into X thread', 'Pull 20 comment hooks', 'Draft Reels caption set', 'Make LinkedIn carousel outline', 'Plan 7 Shorts ideas'],
},
{
pattern: /(browser|web)/i,
diff --git a/tests.md b/tests.md
index 0e8eb7983..c4541efcd 100644
--- a/tests.md
+++ b/tests.md
@@ -350,7 +350,7 @@ Plugin catalog popular examples.
1. In light theme, open `/#/skills?tab=plugins`.
2. Confirm plugin cards are sorted by `Popular` by default.
3. Confirm the first plugin cards follow the hardcoded casual-user popularity order when those plugins are available, prioritizing email, calendar, Drive, Docs, Sheets, Notion, Obsidian, and social apps such as Reddit, X/Twitter, YouTube, Instagram, TikTok, Facebook, and LinkedIn before falling back to heuristic scoring.
-4. Confirm each visible plugin card shows five concrete example chips, such as inbox triage, focus-block planning, second-brain search, social listening, content repurposing, or fallback action examples, instead of only generic category/capability chips.
+4. Confirm each visible plugin card shows five concrete example chips, such as `Find invoices due this week`, `Protect 3 focus blocks`, `Roll up weekly daily notes`, `Find 10 SaaS complaints`, or `Turn video into X thread`, instead of generic category/capability chips.
5. Open an installed plugin detail and confirm the same five example chips are shown in the `Examples` section.
6. Open a plugin that is not installed or is unavailable for install and confirm its example chips remain visible in the card and detail view.
7. Search by text from an example chip and confirm matching plugins remain discoverable.
From 80d269457bbd4e36793a316451ffc5bb5d4282fa Mon Sep 17 00:00:00 2001
From: Igor
Date: Wed, 13 May 2026 08:35:18 +0700
Subject: [PATCH 8/8] Make directory examples actionable
---
src/App.vue | 3 +-
src/components/content/DirectoryHub.vue | 186 ++++++++++++++++--------
tests.md | 26 ++--
3 files changed, 143 insertions(+), 72 deletions(-)
diff --git a/src/App.vue b/src/App.vue
index 2773c17ba..8eb9b785a 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1208,6 +1208,7 @@ type DirectoryTryItemPayload = {
displayName: string
skillPath?: string
prompt?: string
+ tryKey?: string
attachedSkills?: Array<{ name: string; path: string }>
}
@@ -4478,7 +4479,7 @@ function buildDirectoryTryPrompt(payload: DirectoryTryItemPayload): string {
}
function getDirectoryTryItemKey(payload: DirectoryTryItemPayload): string {
- return `${payload.kind}:${payload.name}:${payload.skillPath ?? ''}`
+ return payload.tryKey ?? `${payload.kind}:${payload.name}:${payload.skillPath ?? ''}:${payload.prompt ?? ''}`
}
async function onTryDirectoryItem(payload: DirectoryTryItemPayload): Promise {
diff --git a/src/components/content/DirectoryHub.vue b/src/components/content/DirectoryHub.vue
index 5d81cd7d1..1e628ef40 100644
--- a/src/components/content/DirectoryHub.vue
+++ b/src/components/content/DirectoryHub.vue
@@ -72,13 +72,11 @@
Loading plugins...
No plugins found.
-
{{ plugin.description }}
-
-
{{ example }}
+
+
+ {{ props.tryInFlightKey === pluginTryKey(plugin, example) ? 'Starting...' : example }}
+
-
+
+
+ Details
+
+
+ Install to run
+
+
+ Enable examples
+
+
+
@@ -165,8 +194,18 @@
{{ app.description }}
-
-
{{ example }}
+
+
+ {{ props.tryInFlightKey === appTryKey(app, example) ? 'Starting...' : example }}
+
@@ -175,15 +214,6 @@
{{ app.isAccessible ? 'Manage' : 'Login' }}
-
- {{ props.tryInFlightKey === appTryKey(app) ? 'Starting...' : 'Try it!' }}
-
@@ -322,8 +352,18 @@
{{ connector.description }}
-
-
{{ example }}
+
+
+ {{ props.tryInFlightKey === composioTryKey(connector.slug, example) ? 'Starting...' : example }}
+
@@ -338,15 +378,6 @@
>
{{ composioActionSlug === connector.slug ? 'Opening...' : composioPrimaryActionLabel(connector) }}
-
- {{ props.tryInFlightKey === composioTryKey(connector.slug) ? 'Starting...' : 'Try it!' }}
-
@@ -448,7 +479,17 @@
Examples
- {{ example }}
+
+ {{ props.tryInFlightKey === pluginTryKey(selectedPlugin, example) ? 'Starting...' : example }}
+
@@ -543,15 +584,6 @@
>
{{ selectedPlugin.enabled ? 'Disable' : 'Enable' }}
-
- {{ props.tryInFlightKey === pluginTryKey(selectedPlugin) ? 'Starting...' : 'Try it!' }}
-
@@ -586,6 +618,23 @@
{{ selectedComposioDetail.connector.description }}
+
+
Examples
+
+
+ {{ props.tryInFlightKey === composioTryKey(selectedComposioDetail.connector.slug, example) ? 'Starting...' : example }}
+
+
+
+
Overview
@@ -644,15 +693,6 @@
>
{{ composioActionSlug === selectedComposioDetail?.connector.slug ? 'Opening...' : composioPrimaryActionLabel(selectedComposioDetail.connector) }}
-
- {{ props.tryInFlightKey === composioTryKey(selectedComposioDetail.connector.slug) ? 'Starting...' : 'Try it!' }}
-
@@ -877,6 +917,7 @@ export type DirectoryTryItemPayload = {
displayName: string
skillPath?: string
prompt?: string
+ tryKey?: string
attachedSkills?: Array<{ name: string; path: string }>
}
@@ -1451,33 +1492,54 @@ function composioConnectionStatusClass(status: string): string {
return 'is-muted'
}
-function appTryKey(app: DirectoryAppInfo): string {
- return `app:${app.id}:`
+function appTryKey(app: DirectoryAppInfo, example = ''): string {
+ return `app:${app.id}:${example}`
}
-function tryApp(app: DirectoryAppInfo): void {
+function canTryApp(app: DirectoryAppInfo): boolean {
+ return app.isAccessible && app.isEnabled
+}
+
+function buildExamplePrompt(displayName: string, itemType: string, example: string): string {
+ const label = displayName.trim()
+ const task = example.trim()
+ if (!task) return `Test ${label} ${itemType}. Give me a list of what it can do and one useful example.`
+ return `Use ${label} ${itemType} to do this concrete task: ${task}. Do not ask me to fill in placeholders first; inspect what is available, make reasonable assumptions, and show the result or the exact next action you took.`
+}
+
+function tryApp(app: DirectoryAppInfo, example = ''): void {
if (isTryActionInFlight.value) return
+ if (!canTryApp(app)) return
emit('try-item', {
kind: 'app',
name: app.id,
displayName: app.name,
+ prompt: buildExamplePrompt(app.name, 'app', example),
+ tryKey: appTryKey(app, example),
})
}
-function pluginTryKey(plugin: DirectoryPluginSummary): string {
- return `plugin:${plugin.name}:`
+function pluginTryKey(plugin: DirectoryPluginSummary, example = ''): string {
+ return `plugin:${plugin.name}:${example}`
}
-function composioTryKey(slug: string): string {
- return `composio:${slug}:`
+function composioTryKey(slug: string, example = ''): string {
+ return `composio:${slug}:${example}`
}
-function tryPlugin(plugin: DirectoryPluginSummary): void {
+function canTryPlugin(plugin: DirectoryPluginSummary): boolean {
+ return plugin.installed && plugin.enabled
+}
+
+function tryPlugin(plugin: DirectoryPluginSummary, example = ''): void {
if (isTryActionInFlight.value) return
+ if (!canTryPlugin(plugin)) return
emit('try-item', {
kind: 'plugin',
name: plugin.name,
displayName: plugin.displayName,
+ prompt: buildExamplePrompt(plugin.displayName, 'plugin', example),
+ tryKey: pluginTryKey(plugin, example),
})
}
@@ -1485,21 +1547,27 @@ function canTryComposio(connector: DirectoryComposioConnector): boolean {
return composioHasUsableConnection(connector)
}
-function buildComposioTryPrompt(connector: DirectoryComposioConnector, connections: DirectoryComposioConnection[] = []): string {
+function buildComposioTryPrompt(connector: DirectoryComposioConnector, connections: DirectoryComposioConnection[] = [], example = ''): string {
const firstActive = connections.find((connection) => connection.status === 'ACTIVE' && !connection.isDisabled)
const accountHint = firstActive?.wordId
? ` If there are multiple accounts, prefer \`${firstActive.wordId}\`.`
: ''
+ const task = example.trim()
+ if (task) {
+ return `Use the Composio CLI skill with the ${connector.name} connector (${connector.slug}) to do this concrete task: ${task}. Do not ask me to fill in placeholders first; inspect available tools/connections, make reasonable assumptions, and show the result or the exact next action you took.${accountHint}`
+ }
return `Use the Composio CLI skill with the ${connector.name} connector (${connector.slug}). Start by listing what it can do here, mention the current connection status, and suggest one safe command I can run now.${accountHint}`
}
-function tryComposio(connector: DirectoryComposioConnector, connections: DirectoryComposioConnection[] = []): void {
+function tryComposio(connector: DirectoryComposioConnector, connections: DirectoryComposioConnection[] = [], example = ''): void {
if (isTryActionInFlight.value) return
+ if (!canTryComposio(connector)) return
emit('try-item', {
kind: 'composio',
name: connector.slug,
displayName: connector.name,
- prompt: buildComposioTryPrompt(connector, connections),
+ prompt: buildComposioTryPrompt(connector, connections, example),
+ tryKey: composioTryKey(connector.slug, example),
attachedSkills: [{ name: 'composio-cli', path: COMPOSIO_SKILL_PATH }],
})
}
@@ -2169,7 +2237,7 @@ button.directory-card {
}
.directory-example-chip {
- @apply border-blue-100 bg-blue-50 text-blue-700;
+ @apply cursor-pointer border-blue-100 bg-blue-50 text-blue-700 transition hover:border-blue-200 hover:bg-blue-100 hover:text-blue-800 disabled:cursor-not-allowed disabled:border-zinc-200 disabled:bg-zinc-50 disabled:text-zinc-400 disabled:opacity-70;
}
.directory-card-actions {
diff --git a/tests.md b/tests.md
index c4541efcd..b4ea6618b 100644
--- a/tests.md
+++ b/tests.md
@@ -350,20 +350,22 @@ Plugin catalog popular examples.
1. In light theme, open `/#/skills?tab=plugins`.
2. Confirm plugin cards are sorted by `Popular` by default.
3. Confirm the first plugin cards follow the hardcoded casual-user popularity order when those plugins are available, prioritizing email, calendar, Drive, Docs, Sheets, Notion, Obsidian, and social apps such as Reddit, X/Twitter, YouTube, Instagram, TikTok, Facebook, and LinkedIn before falling back to heuristic scoring.
-4. Confirm each visible plugin card shows five concrete example chips, such as `Find invoices due this week`, `Protect 3 focus blocks`, `Roll up weekly daily notes`, `Find 10 SaaS complaints`, or `Turn video into X thread`, instead of generic category/capability chips.
-5. Open an installed plugin detail and confirm the same five example chips are shown in the `Examples` section.
-6. Open a plugin that is not installed or is unavailable for install and confirm its example chips remain visible in the card and detail view.
-7. Search by text from an example chip and confirm matching plugins remain discoverable.
-8. Open the Apps tab and confirm `Popular` uses the hardcoded casual-user app order for available top entries, including social apps, then heuristic scoring for the rest; verify app cards show the same five creative example-chip style instead of category/plugin-name chips.
-9. Open the Composio tab after login and confirm `Popular` uses the hardcoded casual-user connector order for available top entries, including social connectors, then heuristic scoring for the rest; verify connector cards show creative example chips instead of only tool/auth metadata chips.
-10. Reload the app on a non-Skills route, wait two seconds, then open the Skills route and confirm Plugins, Apps, and the first Composio page use preloaded catalog data instead of showing a long initial loading pause.
-11. Use Refresh or perform a plugin install/uninstall/enable/disable action and confirm the catalog fetches fresh data instead of staying pinned to the preloaded cache.
-12. Switch to dark theme and repeat steps 1-11.
-
-#### Expected Results
-- Plugin examples are visible before installation and are not gated by installed/enabled state.
+4. Confirm each visible plugin card shows five concrete example buttons, such as `Find invoices due this week`, `Protect 3 focus blocks`, `Roll up weekly daily notes`, `Find 10 SaaS complaints`, or `Turn video into X thread`, instead of generic category/capability chips.
+5. Click one enabled plugin example button and confirm it starts a new chat with a task-specific prompt instead of the old generic `Try it!` prompt.
+6. Open an installed plugin detail and confirm the same five example buttons are shown in the `Examples` section and are clickable.
+7. Open a plugin that is not installed or is unavailable for install and confirm its example buttons remain visible but disabled until install/enable.
+8. Search by text from an example button and confirm matching plugins remain discoverable.
+9. Open the Apps tab and confirm `Popular` uses the hardcoded casual-user app order for available top entries, including social apps, then heuristic scoring for the rest; verify app cards show clickable example buttons instead of category/plugin-name chips and no generic `Try it!` button.
+10. Open the Composio tab after login and confirm `Popular` uses the hardcoded casual-user connector order for available top entries, including social connectors, then heuristic scoring for the rest; verify connector cards show clickable example buttons instead of only tool/auth metadata chips and no generic `Try it!` button.
+11. Reload the app on a non-Skills route, wait two seconds, then open the Skills route and confirm Plugins, Apps, and the first Composio page use preloaded catalog data instead of showing a long initial loading pause.
+12. Use Refresh or perform a plugin install/uninstall/enable/disable action and confirm the catalog fetches fresh data instead of staying pinned to the preloaded cache.
+13. Switch to dark theme and repeat steps 1-12.
+
+#### Expected Results
+- Plugin examples are visible before installation, but only installed/enabled plugins run example tasks.
- Popular plugin families show practical examples tailored to their domain.
- Apps and Composio connector cards also show five domain-specific example chips for top everyday workflows.
+- Example chips replace generic card-level `Try it!` buttons and start task-specific chats when clicked.
- Plugin, Apps, and Composio popular sorting put the hardcoded casual-user top 20 entries first when available, with browser, Chrome, and computer-use entries left to heuristic sorting instead of pinned ranking.
- Rows outside the hardcoded top 20 continue to use the existing heuristic popularity score.
- Plugin, Apps, and Composio catalog data is preloaded in the background and reused when opening the Skills route.