UNPKG

@earendil-works/pi-coding-agent

Version:

Coding agent CLI with read, bash, edit, write tools and session management

155 lines 5.07 kB
import { fuzzyMatch } from "@earendil-works/pi-tui"; function normalizeWhitespaceLower(text) { return text.toLowerCase().replace(/\s+/g, " ").trim(); } function getSessionSearchText(session) { return `${session.id} ${session.name ?? ""} ${session.allMessagesText} ${session.cwd}`; } export function hasSessionName(session) { return Boolean(session.name?.trim()); } function matchesNameFilter(session, filter) { if (filter === "all") return true; return hasSessionName(session); } export function parseSearchQuery(query) { const trimmed = query.trim(); if (!trimmed) { return { mode: "tokens", tokens: [], regex: null }; } // Regex mode: re:<pattern> if (trimmed.startsWith("re:")) { const pattern = trimmed.slice(3).trim(); if (!pattern) { return { mode: "regex", tokens: [], regex: null, error: "Empty regex" }; } try { return { mode: "regex", tokens: [], regex: new RegExp(pattern, "i") }; } catch (err) { const msg = err instanceof Error ? err.message : String(err); return { mode: "regex", tokens: [], regex: null, error: msg }; } } // Token mode with quote support. // Example: foo "node cve" bar const tokens = []; let buf = ""; let inQuote = false; let hadUnclosedQuote = false; const flush = (kind) => { const v = buf.trim(); buf = ""; if (!v) return; tokens.push({ kind, value: v }); }; for (let i = 0; i < trimmed.length; i++) { const ch = trimmed[i]; if (ch === '"') { if (inQuote) { flush("phrase"); inQuote = false; } else { flush("fuzzy"); inQuote = true; } continue; } if (!inQuote && /\s/.test(ch)) { flush("fuzzy"); continue; } buf += ch; } if (inQuote) { hadUnclosedQuote = true; } // If quotes were unbalanced, fall back to plain whitespace tokenization. if (hadUnclosedQuote) { return { mode: "tokens", tokens: trimmed .split(/\s+/) .map((t) => t.trim()) .filter((t) => t.length > 0) .map((t) => ({ kind: "fuzzy", value: t })), regex: null, }; } flush(inQuote ? "phrase" : "fuzzy"); return { mode: "tokens", tokens, regex: null }; } export function matchSession(session, parsed) { const text = getSessionSearchText(session); if (parsed.mode === "regex") { if (!parsed.regex) { return { matches: false, score: 0 }; } const idx = text.search(parsed.regex); if (idx < 0) return { matches: false, score: 0 }; return { matches: true, score: idx * 0.1 }; } if (parsed.tokens.length === 0) { return { matches: true, score: 0 }; } let totalScore = 0; let normalizedText = null; for (const token of parsed.tokens) { if (token.kind === "phrase") { if (normalizedText === null) { normalizedText = normalizeWhitespaceLower(text); } const phrase = normalizeWhitespaceLower(token.value); if (!phrase) continue; const idx = normalizedText.indexOf(phrase); if (idx < 0) return { matches: false, score: 0 }; totalScore += idx * 0.1; continue; } const m = fuzzyMatch(token.value, text); if (!m.matches) return { matches: false, score: 0 }; totalScore += m.score; } return { matches: true, score: totalScore }; } export function filterAndSortSessions(sessions, query, sortMode, nameFilter = "all") { const nameFiltered = nameFilter === "all" ? sessions : sessions.filter((session) => matchesNameFilter(session, nameFilter)); const trimmed = query.trim(); if (!trimmed) return nameFiltered; const parsed = parseSearchQuery(query); if (parsed.error) return []; // Recent mode: filter only, keep incoming order. if (sortMode === "recent") { const filtered = []; for (const s of nameFiltered) { const res = matchSession(s, parsed); if (res.matches) filtered.push(s); } return filtered; } // Relevance mode: sort by score, tie-break by modified desc. const scored = []; for (const s of nameFiltered) { const res = matchSession(s, parsed); if (!res.matches) continue; scored.push({ session: s, score: res.score }); } scored.sort((a, b) => { if (a.score !== b.score) return a.score - b.score; return b.session.modified.getTime() - a.session.modified.getTime(); }); return scored.map((r) => r.session); } //# sourceMappingURL=session-selector-search.js.map