Trigger /<name>, dropdown lists all skills filtered by name prefix, arg passthrough sends the rest as the user message. Synthetic skill_use tool_use renders identically to model-invoked skills.
44 lines
1.4 KiB
TypeScript
44 lines
1.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { api } from '@/api/client';
|
|
import type { Skill } from '@/api/types';
|
|
|
|
// Batch 9.6: shared in-memory cache for the slash-command dropdown. One fetch
|
|
// per process; subsequent mounts of useSkills() return the cached list and
|
|
// don't re-hit /api/skills. Matches the useSidebar / useChatStatus module-
|
|
// singleton pattern so the dropdown stays cheap even with many ChatInputs
|
|
// mounted at once.
|
|
|
|
let cachedSkills: Skill[] | null = null;
|
|
let inflight: Promise<Skill[]> | null = null;
|
|
const subscribers = new Set<(s: Skill[]) => void>();
|
|
|
|
async function loadSkills(): Promise<Skill[]> {
|
|
if (inflight) return inflight;
|
|
inflight = api.skills
|
|
.list()
|
|
.then((r) => {
|
|
cachedSkills = r.skills;
|
|
for (const sub of subscribers) {
|
|
try { sub(cachedSkills); } catch { /* swallow */ }
|
|
}
|
|
return cachedSkills;
|
|
})
|
|
.finally(() => { inflight = null; });
|
|
return inflight;
|
|
}
|
|
|
|
export function useSkills(): { skills: Skill[]; loaded: boolean } {
|
|
const [skills, setSkills] = useState<Skill[]>(cachedSkills ?? []);
|
|
const [loaded, setLoaded] = useState<boolean>(cachedSkills !== null);
|
|
|
|
useEffect(() => {
|
|
subscribers.add(setSkills);
|
|
if (cachedSkills === null) {
|
|
void loadSkills().then(() => setLoaded(true)).catch(() => setLoaded(true));
|
|
}
|
|
return () => { subscribers.delete(setSkills); };
|
|
}, []);
|
|
|
|
return { skills, loaded };
|
|
}
|