Download packages/coding-agent/src/modes/interactive/components/session-selector-search.ts from SaylorTwift/pi: direct link, hf CLI and curl.
- Browser
- Download file 5.1 kB
-
https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/src/modes/interactive/components/session-selector-search.ts
- Command line
-
hf download hf://SaylorTwift/pi/packages/coding-agent/src/modes/interactive/components/session-selector-search.ts
-
curl -L -o session-selector-search.ts https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/src/modes/interactive/components/session-selector-search.ts
5.1 kB
| import { fuzzyMatch } from "@earendil-works/pi-tui"; | |
| import type { SessionInfo } from "../../../core/session-manager.ts"; | |
| export type SortMode = "threaded" | "recent" | "relevance"; | |
| export type NameFilter = "all" | "named"; | |
| export interface ParsedSearchQuery { | |
| mode: "tokens" | "regex"; | |
| tokens: { kind: "fuzzy" | "phrase"; value: string }[]; | |
| regex: RegExp | null; | |
| /** If set, parsing failed and we should treat query as non-matching. */ | |
| error?: string; | |
| } | |
| export interface MatchResult { | |
| matches: boolean; | |
| /** Lower is better; only meaningful when matches === true */ | |
| score: number; | |
| } | |
| function normalizeWhitespaceLower(text: string): string { | |
| return text.toLowerCase().replace(/\s+/g, " ").trim(); | |
| } | |
| function getSessionSearchText(session: SessionInfo): string { | |
| return `${session.id} ${session.name ?? ""} ${session.allMessagesText} ${session.cwd}`; | |
| } | |
| export function hasSessionName(session: SessionInfo): boolean { | |
| return Boolean(session.name?.trim()); | |
| } | |
| function matchesNameFilter(session: SessionInfo, filter: NameFilter): boolean { | |
| if (filter === "all") return true; | |
| return hasSessionName(session); | |
| } | |
| export function parseSearchQuery(query: string): ParsedSearchQuery { | |
| 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: { kind: "fuzzy" | "phrase"; value: string }[] = []; | |
| let buf = ""; | |
| let inQuote = false; | |
| let hadUnclosedQuote = false; | |
| const flush = (kind: "fuzzy" | "phrase"): void => { | |
| 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" as const, value: t })), | |
| regex: null, | |
| }; | |
| } | |
| flush(inQuote ? "phrase" : "fuzzy"); | |
| return { mode: "tokens", tokens, regex: null }; | |
| } | |
| export function matchSession(session: SessionInfo, parsed: ParsedSearchQuery): MatchResult { | |
| 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: string | null = 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: SessionInfo[], | |
| query: string, | |
| sortMode: SortMode, | |
| nameFilter: NameFilter = "all", | |
| ): SessionInfo[] { | |
| 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: SessionInfo[] = []; | |
| 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: { session: SessionInfo; score: number }[] = []; | |
| 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); | |
| } | |