fix(chat): query expert skills via symlinks and use $ prefix for Codex

Expert skills in the chat session were derived by intersecting built-in
experts with ACP availableCommands, which caused Codex experts to never
appear since Codex does not advertise skills through ACP.

- Add `experts_list_for_agent` backend API that checks symlink status
  across all global skill dirs for the given agent type
- Replace availableCommands-based expert filtering with symlink-based
  query, making the settings page the single source of truth
- Use `$` prefix for Codex expert skills while keeping `/` for slash
  commands and other agents' experts
- Disable the expert button when no experts are linked for the agent
- Invalidate per-agent expert cache after link/unlink in settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-11 00:13:42 +08:00
parent e4eb7f67eb
commit ade59f474c
11 changed files with 187 additions and 29 deletions

View File

@@ -0,0 +1,68 @@
"use client"
import { useEffect, useMemo, useState } from "react"
import { expertsListForAgent } from "@/lib/api"
import type { AgentType, ExpertListItem } from "@/lib/types"
const agentCache = new Map<AgentType, ExpertListItem[]>()
const inflightMap = new Map<AgentType, Promise<ExpertListItem[]>>()
const EMPTY: ExpertListItem[] = []
export function useAgentExperts(agentType: AgentType | null): ExpertListItem[] {
const cached = useMemo(
() => (agentType ? (agentCache.get(agentType) ?? null) : null),
[agentType]
)
// Track which agent type the fetched result belongs to so stale data
// from a previous agent is never returned after a switch.
const [fetched, setFetched] = useState<{
agentType: AgentType
experts: ExpertListItem[]
} | null>(null)
useEffect(() => {
if (!agentType || agentCache.has(agentType)) return
let cancelled = false
let promise = inflightMap.get(agentType)
if (!promise) {
promise = expertsListForAgent(agentType)
.then((list) => {
agentCache.set(agentType, list)
inflightMap.delete(agentType)
return list
})
.catch((err) => {
inflightMap.delete(agentType)
console.warn("[useAgentExperts] failed:", err)
return EMPTY
})
inflightMap.set(agentType, promise)
}
promise.then((list) => {
if (!cancelled) setFetched({ agentType, experts: list })
})
return () => {
cancelled = true
}
}, [agentType])
if (!agentType) return EMPTY
if (cached) return cached
if (fetched && fetched.agentType === agentType) return fetched.experts
return EMPTY
}
export function invalidateAgentExpertsCache(agentType?: AgentType) {
if (agentType) {
agentCache.delete(agentType)
inflightMap.delete(agentType)
} else {
agentCache.clear()
inflightMap.clear()
}
}