/* Overlays: command palette (fuzzy file finder), content search, context menu, toast */ function fuzzy(q, str) { q = q.toLowerCase(); const s = str.toLowerCase(); let i = 0; const idx = []; for (let j = 0; j < s.length && i < q.length; j++) { if (s[j] === q[i]) { idx.push(j); i++; } } return i === q.length ? idx : null; } function Highlight({ text, idx }) { if (!idx || !idx.length) return {text}; const set = new Set(idx); return {text.split("").map((ch, i) => set.has(i) ? {ch} : {ch})}; } function SearchModal({ onOpen, onOpenAt, onClose, changeSet }) { const [q, setQ] = useState(""); const [sel, setSel] = useState(0); const inputRef = useRef(null); const leftRef = useRef(null); const allPaths = useMemo(() => Object.keys(PROJECT.files), []); useEffect(() => { inputRef.current && inputRef.current.focus(); }, []); // content hits (left) const content = useMemo(() => { const term = q.trim(); if (term.length < 2) return []; const low = term.toLowerCase(); const groups = []; for (const [path, src] of Object.entries(PROJECT.files)) { const lines = src.split("\n"); const hits = []; lines.forEach((ln, i) => { const ix = ln.toLowerCase().indexOf(low); if (ix >= 0) hits.push({ no: i + 1, ln, ix }); }); if (hits.length) groups.push({ path, hits }); } return groups; }, [q]); // file-name matches (right) const files = useMemo(() => { const term = q.trim(); if (!term) return []; const out = []; for (const p of allPaths) { const name = p.split("/").pop(); const ni = fuzzy(term, name); if (ni) { out.push({ path: p, idx: ni, rank: 0, pos: ni[0] }); continue; } const pi = fuzzy(term, p); if (pi) out.push({ path: p, idx: null, rank: 1, pos: pi[0] }); } out.sort((a, b) => a.rank - b.rank || a.pos - b.pos || a.path.length - b.path.length); return out; }, [q]); // flat list of content hits for keyboard nav const flat = useMemo(() => { const arr = []; content.forEach((g) => g.hits.forEach((h) => arr.push({ path: g.path, no: h.no }))); return arr; }, [content]); const totalHits = flat.length; useEffect(() => { setSel(0); }, [q]); useEffect(() => { const el = leftRef.current && leftRef.current.querySelector(".sr-line.sel"); if (el) el.scrollIntoView({ block: "nearest" }); }, [sel]); function onKey(e) { if (e.key === "ArrowDown") { e.preventDefault(); setSel((s) => Math.min(s + 1, flat.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setSel((s) => Math.max(s - 1, 0)); } else if (e.key === "Enter") { e.preventDefault(); if (flat[sel]) { onOpenAt(flat[sel].path, flat[sel].no); onClose(); } else if (files[0]) { onOpen(files[0].path); onClose(); } } else if (e.key === "Escape") { e.preventDefault(); onClose(); } } function renderLine(ln, ix, len) { const pre = ln.slice(0, ix), mid = ln.slice(ix, ix + len), post = ln.slice(ix + len); return {pre}{mid}{post}; } const term = q.trim(); let flatIx = -1; return (
e.stopPropagation()}>
{Icon.search({ style: { color: "var(--fg-3)" } })} setQ(e.target.value)} onKeyDown={onKey} placeholder="Search content and file names…" spellCheck={false} /> {totalHits} hit{totalHits === 1 ? "" : "s"} · {files.length} file{files.length === 1 ? "" : "s"}
Content {totalHits > 0 && {totalHits}}
{term.length < 2 &&
Type at least 2 characters
} {term.length >= 2 && content.length === 0 &&
No content matches
} {content.map((g) => (
onOpenAt(g.path, g.hits[0].no)}> {g.path} {g.hits.length}
{g.hits.slice(0, 12).map((h) => { flatIx++; const me = flatIx; return (
setSel(me)} onClick={() => { onOpenAt(g.path, h.no); onClose(); }}> {h.no} {renderLine(h.ln, h.ix, term.length)}
); })}
))}
Files {files.length > 0 && {files.length}}
{!term &&
Start typing…
} {term && files.length === 0 &&
No file names match
} {files.slice(0, 40).map((r) => { const name = r.path.split("/").pop(); const dir = r.path.split("/").slice(0, -1).join("/"); return (
{ onOpen(r.path); onClose(); }} title={r.path}>
{dir && {dir}/}
{changeSet.has(r.path) && }
); })}
); } function ContextMenu({ menu, onClose }) { const ref = useRef(null); useEffect(() => { const h = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); }; const k = (e) => { if (e.key === "Escape") onClose(); }; document.addEventListener("mousedown", h); document.addEventListener("keydown", k); return () => { document.removeEventListener("mousedown", h); document.removeEventListener("keydown", k); }; }, []); if (!menu) return null; const x = Math.min(menu.x, window.innerWidth - 270); const y = Math.min(menu.y, window.innerHeight - (menu.items.length * 34 + 60)); return (
{menu.note &&
{menu.note}
} {menu.items.map((it, i) => it.sep ?
: (
{ it.onClick(); onClose(); }}> {it.icon} {it.label} {it.kbd && {it.kbd}}
))}
); } function Toasts({ toasts }) { return (
{toasts.map((t) => (
{Icon.copy({ style: { color: "var(--accent)" } })} {t.title} {t.ref && {t.ref}}
))}
); } function PassPopup({ x, y, refStr, onConfirm, onCancel }) { const [text, setText] = useState(""); const inputRef = useRef(null); const boxRef = useRef(null); useEffect(() => { inputRef.current && inputRef.current.focus(); }, []); useEffect(() => { const h = (e) => { if (boxRef.current && !boxRef.current.contains(e.target)) onCancel(); }; const k = (e) => { if (e.key === "Escape") { e.preventDefault(); onCancel(); } }; document.addEventListener("mousedown", h); document.addEventListener("keydown", k, true); return () => { document.removeEventListener("mousedown", h); document.removeEventListener("keydown", k, true); }; }, []); const left = Math.min(x, window.innerWidth - 360); const top = Math.min(y + 6, window.innerHeight - 150); const preview = (text.trim() ? text.trim() + " " : "") + refStr; return (
{Icon.spark({})}Pass on to Agentesc
setText(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); onConfirm(text); } else if (e.key === "Escape") { e.preventDefault(); onCancel(); } }} />
inserts{preview}
insert into agent · esc cancel
); } Object.assign(window, { SearchModal, ContextMenu, Toasts, PassPopup, fuzzy });