diff --git a/src/App.vue b/src/App.vue
index 034797e70..8eb9b785a 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1144,6 +1144,10 @@ import {
getThreadTerminalQuickCommands,
getThreadTerminalStatus,
getWorkspaceRootsState,
+ getDirectoryComposioStatus,
+ listDirectoryApps,
+ listDirectoryComposioConnectors,
+ listDirectoryPlugins,
listLocalDirectories,
openProjectRoot,
persistFirstLaunchPluginsCardPreference,
@@ -1204,6 +1208,7 @@ type DirectoryTryItemPayload = {
displayName: string
skillPath?: string
prompt?: string
+ tryKey?: string
attachedSkills?: Array<{ name: string; path: string }>
}
@@ -2010,6 +2015,9 @@ onMounted(() => {
void loadFreeModeStatus()
void refreshThreadTerminalStatus()
void refreshTerminalQuickCommands()
+ window.setTimeout(() => {
+ void preloadDirectoryCatalogs()
+ }, 1500)
})
watch(visibleFeedbackErrors, (values, oldValues) => {
@@ -4154,6 +4162,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()
@@ -4453,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/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 beff5537a..1e628ef40 100644
--- a/src/components/content/DirectoryHub.vue
+++ b/src/components/content/DirectoryHub.vue
@@ -72,13 +72,11 @@
Loading plugins...
No plugins found.
-
{{ app.description }}
-
-
{{ app.category }}
-
{{ name }}
+
+
+ {{ props.tryInFlightKey === appTryKey(app, example) ? 'Starting...' : example }}
+
@@ -177,15 +214,6 @@
{{ app.isAccessible ? 'Manage' : 'Login' }}
-
- {{ props.tryInFlightKey === appTryKey(app) ? 'Starting...' : 'Try it!' }}
-
@@ -324,10 +352,18 @@
{{ connector.description }}
-
-
{{ connector.toolsCount }} tools
-
{{ connector.triggersCount }} triggers
-
{{ connector.authModes.join(', ') }}
+
+
+ {{ props.tryInFlightKey === composioTryKey(connector.slug, example) ? 'Starting...' : example }}
+
@@ -342,15 +378,6 @@
>
{{ composioActionSlug === connector.slug ? 'Opening...' : composioPrimaryActionLabel(connector) }}
-
- {{ props.tryInFlightKey === composioTryKey(connector.slug) ? 'Starting...' : 'Try it!' }}
-
@@ -449,6 +476,23 @@
{{ selectedPluginDescription }}
+
+
Examples
+
+
+ {{ props.tryInFlightKey === pluginTryKey(selectedPlugin, example) ? 'Starting...' : example }}
+
+
+
+
Capabilities
@@ -540,15 +584,6 @@
>
{{ selectedPlugin.enabled ? 'Disable' : 'Enable' }}
-
- {{ props.tryInFlightKey === pluginTryKey(selectedPlugin) ? 'Starting...' : 'Try it!' }}
-
@@ -583,6 +618,23 @@
{{ selectedComposioDetail.connector.description }}
+
+
Examples
+
+
+ {{ props.tryInFlightKey === composioTryKey(selectedComposioDetail.connector.slug, example) ? 'Starting...' : example }}
+
+
+
+
Overview
@@ -641,15 +693,6 @@
>
{{ composioActionSlug === selectedComposioDetail?.connector.slug ? 'Opening...' : composioPrimaryActionLabel(selectedComposioDetail.connector) }}
-
- {{ props.tryInFlightKey === composioTryKey(selectedComposioDetail.connector.slug) ? 'Starting...' : 'Try it!' }}
-
@@ -696,6 +739,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]> = [
+ [/^gmail$/i, 20],
+ [/^google calendar$/i, 19],
+ [/^google drive$/i, 18],
+ [/^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],
+ [/^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]> = [
[/^gmail$/i, 30_000],
[/^google calendar$/i, 29_500],
@@ -733,8 +820,86 @@ 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: /(gmail|email|outlook|mail|inbox)/i,
+ 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: ['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 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 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 duplicate leads', 'Flag spend outliers', 'Build revenue pivot', 'Write margin formulas', 'Draft 3-line KPI readout'],
+ },
+ {
+ pattern: /(notion|wiki|page)/i,
+ 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: ['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 10 SaaS complaints', 'Mine r/productivity questions', 'Spot pricing objections', 'Draft non-salesy reply', 'Track competitor pain posts'],
+ },
+ {
+ pattern: /(x|twitter)/i,
+ 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: ['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,
+ 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: /(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],
@@ -752,6 +917,7 @@ export type DirectoryTryItemPayload = {
displayName: string
skillPath?: string
prompt?: string
+ tryKey?: string
attachedSkills?: Array<{ name: string; path: string }>
}
@@ -877,6 +1043,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,10 +1172,100 @@ 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 exampleChipsFromSource(source: string, fallbackLabel: string, fallbackValues: string[] = []): string[] {
+ const chips: string[] = []
+ const seen = new Set()
+
+ 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 value of fallbackValues) {
+ addExampleChip(chips, seen, value.replace(/[.!?]\s*$/u, ''))
+ if (chips.length >= 5) return chips.slice(0, 5)
+ }
+
+ addExampleChip(chips, seen, `Explore ${fallbackLabel}`)
+ 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 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)
}
+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, '')
@@ -1084,6 +1344,8 @@ function filterPlugins(rows: DirectoryPluginSummary[], query: string): Directory
plugin.category,
plugin.marketplaceDisplayName,
...plugin.capabilities,
+ ...plugin.defaultPrompt,
+ ...pluginExampleChips(plugin),
], query))
}
@@ -1095,6 +1357,7 @@ function filterApps(rows: DirectoryAppInfo[], query: string): DirectoryAppInfo[]
app.category,
app.distributionChannel,
...app.pluginDisplayNames,
+ ...appExampleChips(app),
], query))
}
@@ -1105,10 +1368,16 @@ function filterComposioConnectors(rows: DirectoryComposioConnector[], query: str
connector.description,
...connector.authModes,
...connector.connectionStatuses,
+ ...composioExampleChips(connector),
], query))
}
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) +
@@ -1121,6 +1390,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) +
@@ -1221,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),
})
}
@@ -1255,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 }],
})
}
@@ -1301,38 +1599,53 @@ 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, { 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 {
+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, { force })
+ 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 {
+async function loadComposio(append = false, force = false): Promise {
if (isLoadingComposio.value) {
isComposioLoadQueued = true
return
@@ -1341,7 +1654,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 = []
@@ -1350,7 +1663,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
@@ -1396,9 +1709,9 @@ async function refreshMcpStatusesForPluginDetail(): Promise {
}
function refreshActiveTab(forceReload = false): void {
- if (activeTab.value === 'plugins') void loadPlugins()
- if (activeTab.value === 'apps') void loadApps()
- if (activeTab.value === 'composio') void loadComposio()
+ if (activeTab.value === 'plugins') void loadPlugins(forceReload)
+ if (activeTab.value === 'apps') void loadApps(forceReload)
+ if (activeTab.value === 'composio') void loadComposio(false, forceReload)
if (activeTab.value === 'skills') {
if (forceReload && supportsMcpReload.value) void reloadMcps()
else void loadMcps()
@@ -1408,9 +1721,9 @@ 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()
- else if (activeTab.value === 'composio') await loadComposio()
+ if (activeTab.value === 'plugins') await loadPlugins(true)
+ else if (activeTab.value === 'apps') await loadApps(true)
+ 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 {
@@ -1496,7 +1809,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)
}
@@ -1561,7 +1874,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 +1897,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 +1919,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 +1985,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 +2232,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 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 {
@apply mt-auto flex items-center gap-2 pt-1;
}
@@ -2205,6 +2526,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/src/components/content/directoryHubUtils.ts b/src/components/content/directoryHubUtils.ts
index c39369563..30223d285 100644
--- a/src/components/content/directoryHubUtils.ts
+++ b/src/components/content/directoryHubUtils.ts
@@ -2,8 +2,30 @@ 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],
+ [/^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],
]
@@ -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 2558d0dfa..b4ea6618b 100644
--- a/tests.md
+++ b/tests.md
@@ -336,6 +336,48 @@ 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 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 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.
+- 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.
+
+#### Rollback/Cleanup
+- None.
+
+---
+
### Qodo feedback diagnostics reliability fixes
#### Feature/Change Name