import { useState, useEffect, useRef } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import { toast } from 'sonner'; import { ArrowLeft, Pencil, Plus, Check, X, Trash2, Search, ChevronRight } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { useClient } from 'hooks/useClient'; import { Card } from '@/components/Card'; import { usePiChat, EmbeddableChat } from 'officerdev'; type CapabilitySummary = { dirName: string; name: string; description: string; scope: 'global' | 'user'; }; export type CapabilityDetail = CapabilitySummary & { body: string; rawFrontmatter: string; filePath: string; chatSessionId: string | null; }; type CapabilityListProps = { kind: string; endpoint: string; queryKey: string; selected: string | null; onSelect: (dirName: string) => void; onCreate?: (dirName: string) => void; search?: string; showCreate?: boolean; onShowCreateChange?: (value: boolean) => void; }; type CapabilityPageProps = { kind: string; endpoint: string; queryKey: string; }; type CapabilityChatProps = { kind: string; endpoint: string; dirName: string; filePath: string; resourceDir: string; chatSessionId: string | null; isNew?: boolean; description?: string; onResponseEnd?: () => void; }; export const CapabilityChat = ({ kind, filePath, resourceDir, isNew, description, onResponseEnd, }: CapabilityChatProps) => { const seedFile = `${kind.toUpperCase()}.md`; const promptFrontmatter = `\ninput file: ${filePath}\n${seedFile}: ${filePath}\ndir: ${resourceDir}\n\nBe aware of any extra files alongside the same dir as the ${kind} file we're handling, for possible extra context. You can also, if pertinent, create scripts or other files that will help you in the future.\n`; const defaultInput = isNew ? description ?? `Help me create the content for this new ${kind} file` : `Help me understand and improve this ${kind} file`; const pi = usePiChat(undefined, undefined, { replaceUrl: false }); const onResponseEndRef = useRef(onResponseEnd); onResponseEndRef.current = onResponseEnd; const wasGenerating = useRef(false); useEffect(() => { if (wasGenerating.current && !pi.isGenerating) { onResponseEndRef.current?.(); } wasGenerating.current = pi.isGenerating; }, [pi.isGenerating]); return ( ); }; export const FrontmatterBlock = ({ yaml }: { yaml: string }) => { const [open, setOpen] = useState(false); return (
{open && (
{yaml}
)}
); }; export const CapabilityList = ({ kind, endpoint, queryKey, selected, onSelect, onCreate, search: externalSearch, showCreate, onShowCreateChange }: CapabilityListProps) => { const client = useClient(); const qc = useQueryClient(); const [internalCreating, setInternalCreating] = useState(false); const creating = showCreate ?? internalCreating; const setCreating = onShowCreateChange ?? setInternalCreating; const [newName, setNewName] = useState(''); const [internalSearch, setInternalSearch] = useState(''); const search = externalSearch ?? internalSearch; const newNameRef = useRef(null); const { data: items = [] } = useQuery({ queryKey: [queryKey], queryFn: () => client.get(endpoint), }); const handleCreate = async () => { const name = newName.trim(); if (!name) return; try { const res = await client.post<{ name: string; dirName: string }>(endpoint, { name }); await qc.invalidateQueries({ queryKey: [queryKey] }); setCreating(false); setNewName(''); (onCreate ?? onSelect)(res.dirName); } catch { toast.error(`Failed to create ${kind}`); } }; const filtered = items.filter( (item) => !search || item.name.toLowerCase().includes(search.toLowerCase()) || item.description?.toLowerCase().includes(search.toLowerCase()), ); return ( <>
{kind}s {!creating && ( )}
{creating && (
setNewName(ev.target.value)} onKeyDown={(ev) => { if (ev.key === 'Enter') { ev.preventDefault(); handleCreate(); } if (ev.key === 'Escape') { setCreating(false); setNewName(''); } }} placeholder={`${kind} name...`} className="flex-1 min-w-0 rounded border border-duck-dark/20 bg-background px-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30" />
)} {externalSearch === undefined && (
setInternalSearch(ev.target.value)} placeholder={`Search ${kind.toLowerCase()}s...`} className="w-full rounded border border-duck-dark/15 bg-background/80 pl-7 pr-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30" />
)}
{filtered.map((item) => ( ))} {items.length === 0 && (

No {kind.toLowerCase()}s found

)} {items.length > 0 && filtered.length === 0 && (

No matches

)}
); }; export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps) => { const client = useClient(); const qc = useQueryClient(); const [selected, setSelected] = useState(null); const [editing, setEditing] = useState(false); const [isNew, setIsNew] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState(false); const [showDetail, setShowDetail] = useState(false); const { data: items = [] } = useQuery({ queryKey: [queryKey], queryFn: () => client.get(endpoint), }); useEffect(() => { if (items.length > 0 && !selected) { setSelected(items[0]!.dirName); } }, [items, selected]); const { data: detail } = useQuery({ queryKey: [queryKey, selected], queryFn: () => client.get(`${endpoint}/${selected}`), enabled: !!selected, }); const selectItem = (dirName: string) => { setSelected(dirName); setShowDetail(true); setIsNew(false); setEditing(false); }; const handleCreate = (dirName: string) => { setSelected(dirName); setShowDetail(true); setIsNew(true); setEditing(true); }; const handleDelete = async () => { if (!selected) return; try { await client.delete(`${endpoint}/${selected}`); setDeleteConfirm(false); setEditing(false); setSelected(null); setShowDetail(false); await qc.invalidateQueries({ queryKey: [queryKey] }); } catch { toast.error(`Failed to delete ${kind}`); } }; return ( <>
{/* Left panel — list */} {/* Right panel — detail + chat */}
{detail?.name ?? `Select a ${kind.toLowerCase()}`} {detail && ( <> )}
{detail?.rawFrontmatter && } {detail?.body ? (
{detail.body}
) : detail ? (

Empty file

) : (

Select a {kind.toLowerCase()} to view its contents

)}
{editing && detail?.filePath && selected && (
{detail?.name ?? 'Chat'}
qc.invalidateQueries({ queryKey: [queryKey, selected] })} />
)}
Delete {kind} Are you sure you want to delete "{detail?.name}"? This action cannot be undone.
); };