fix(chat): use ~/.codex/skills/ for Codex expert symlinks and keep expert button always enabled

Change Codex skill storage to use only ~/.codex/skills/ instead of
~/.agents/skills/, and never disable the expert skills button so users
can always access the empty-state hint. Also fix stale expert list in
conversation window after linking in the separate settings window by
re-fetching on window focus.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-11 00:37:31 +08:00
parent ade59f474c
commit 19979d50d0
4 changed files with 44 additions and 30 deletions

View File

@@ -1213,11 +1213,8 @@ pub(crate) fn skill_storage_spec(agent_type: AgentType) -> Option<SkillStorageSp
}), }),
AgentType::Codex => Some(SkillStorageSpec { AgentType::Codex => Some(SkillStorageSpec {
kind: SkillStorageKind::SkillDirectoryOrMarkdownFile, kind: SkillStorageKind::SkillDirectoryOrMarkdownFile,
global_dirs: vec![ global_dirs: vec![codex_home_dir().join("skills")],
home_dir_or_default().join(".agents").join("skills"), project_rel_dirs: vec![".codex/skills"],
codex_home_dir().join("skills"),
],
project_rel_dirs: vec![".agents/skills", ".codex/skills"],
}), }),
AgentType::OpenCode => Some(SkillStorageSpec { AgentType::OpenCode => Some(SkillStorageSpec {
kind: SkillStorageKind::SkillDirectoryOnly, kind: SkillStorageKind::SkillDirectoryOnly,

View File

@@ -851,9 +851,8 @@ pub async fn experts_unlink_from_agent(
let _guard = mutation_lock().lock().await; let _guard = mutation_lock().lock().await;
// Scan ALL global dirs for this agent to handle shared-dir agents // Scan ALL global dirs for this agent to handle shared-dir agents.
// (Codex and Cline both point at `~/.agents/skills/`). Remove the // Remove the link wherever it is found.
// link wherever it is found.
let dirs = scoped_skill_dirs(agent_type, AgentSkillScope::Global, None) let dirs = scoped_skill_dirs(agent_type, AgentSkillScope::Global, None)
.map_err(|_| ExpertsError::UnsupportedAgent(agent_type))?; .map_err(|_| ExpertsError::UnsupportedAgent(agent_type))?;

View File

@@ -1757,7 +1757,7 @@ export function MessageInput({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
disabled={disabled || availableExperts.length === 0} disabled={disabled}
variant="outline" variant="outline"
size="icon" size="icon"
className="h-6 w-6 shrink-0 bg-transparent" className="h-6 w-6 shrink-0 bg-transparent"

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useEffect, useMemo, useState } from "react" import { useCallback, useEffect, useMemo, useState } from "react"
import { expertsListForAgent } from "@/lib/api" import { expertsListForAgent } from "@/lib/api"
import type { AgentType, ExpertListItem } from "@/lib/types" import type { AgentType, ExpertListItem } from "@/lib/types"
@@ -10,6 +10,25 @@ const inflightMap = new Map<AgentType, Promise<ExpertListItem[]>>()
const EMPTY: ExpertListItem[] = [] const EMPTY: ExpertListItem[] = []
function fetchForAgent(agentType: AgentType): Promise<ExpertListItem[]> {
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)
}
return promise
}
export function useAgentExperts(agentType: AgentType | null): ExpertListItem[] { export function useAgentExperts(agentType: AgentType | null): ExpertListItem[] {
const cached = useMemo( const cached = useMemo(
() => (agentType ? (agentCache.get(agentType) ?? null) : null), () => (agentType ? (agentCache.get(agentType) ?? null) : null),
@@ -22,35 +41,34 @@ export function useAgentExperts(agentType: AgentType | null): ExpertListItem[] {
experts: ExpertListItem[] experts: ExpertListItem[]
} | null>(null) } | null>(null)
useEffect(() => { const doFetch = useCallback(() => {
if (!agentType || agentCache.has(agentType)) return if (!agentType || agentCache.has(agentType)) return
let cancelled = false let cancelled = false
let promise = inflightMap.get(agentType) fetchForAgent(agentType).then((list) => {
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 }) if (!cancelled) setFetched({ agentType, experts: list })
}) })
return () => { return () => {
cancelled = true cancelled = true
} }
}, [agentType]) }, [agentType])
// Initial fetch
useEffect(() => doFetch(), [doFetch])
// Re-fetch when window regains focus (covers cross-window cache
// invalidation — e.g. settings window links/unlinks experts while the
// conversation window stays mounted).
useEffect(() => {
const onFocus = () => {
if (!agentType) return
agentCache.delete(agentType)
inflightMap.delete(agentType)
doFetch()
}
window.addEventListener("focus", onFocus)
return () => window.removeEventListener("focus", onFocus)
}, [agentType, doFetch])
if (!agentType) return EMPTY if (!agentType) return EMPTY
if (cached) return cached if (cached) return cached
if (fetched && fetched.agentType === agentType) return fetched.experts if (fetched && fetched.agentType === agentType) return fetched.experts