less-style / bar. 1-row, same border-top styling as TranscriptModeFooter * so swapping them in the bottom slot doesn't shift ScrollBox height. * useSearchInput handles readline editing; we report query changes and * render the counter. Incremental — re-search + highlight per keystroke.
({
jumpRef,
count,
current,
onClose,
onCancel,
setHighlight,
initialQuery
}: {
jumpRef: RefObject<JumpHandle | null>;
count: number;
current: number;
/** Enter — commit. Query persists for n/N. */
onClose: (lastQuery: string) => void;
/** Esc/ctrl+c/ctrl+g — undo to pre-/ state. */
onCancel: () => void;
setHighlight: (query: string) => void;
// Seed with the previous query (less: / shows last pattern). Mount-fire
// of the effect re-scans with the same query — idempotent (same matches,
// nearest-ptr, same highlights). User can edit or clear.
initialQuery: string;
})
| 366 | * useSearchInput handles readline editing; we report query changes and |
| 367 | * render the counter. Incremental — re-search + highlight per keystroke. */ |
| 368 | function TranscriptSearchBar({ |
| 369 | jumpRef, |
| 370 | count, |
| 371 | current, |
| 372 | onClose, |
| 373 | onCancel, |
| 374 | setHighlight, |
| 375 | initialQuery |
| 376 | }: { |
| 377 | jumpRef: RefObject<JumpHandle | null>; |
| 378 | count: number; |
| 379 | current: number; |
| 380 | /** Enter — commit. Query persists for n/N. */ |
| 381 | onClose: (lastQuery: string) => void; |
| 382 | /** Esc/ctrl+c/ctrl+g — undo to pre-/ state. */ |
| 383 | onCancel: () => void; |
| 384 | setHighlight: (query: string) => void; |
| 385 | // Seed with the previous query (less: / shows last pattern). Mount-fire |
| 386 | // of the effect re-scans with the same query — idempotent (same matches, |
| 387 | // nearest-ptr, same highlights). User can edit or clear. |
| 388 | initialQuery: string; |
| 389 | }): React.ReactNode { |
| 390 | const { |
| 391 | query, |
| 392 | cursorOffset |
| 393 | } = useSearchInput({ |
| 394 | isActive: true, |
| 395 | initialQuery, |
| 396 | onExit: () => onClose(query), |
| 397 | onCancel |
| 398 | }); |
| 399 | // Index warm-up runs before the query effect so it measures the real |
| 400 | // cost — otherwise setSearchQuery fills the cache first and warm |
| 401 | // reports ~0ms while the user felt the actual lag. |
| 402 | // First / in a transcript session pays the extractSearchText cost. |
| 403 | // Subsequent / return 0 immediately (indexWarmed ref in VML). |
| 404 | // Transcript is frozen at ctrl+o so the cache stays valid. |
| 405 | // Initial 'building' so warmDone is false on mount — the [query] effect |
| 406 | // waits for the warm effect's first resolve instead of racing it. With |
| 407 | // null initial, warmDone would be true on mount → [query] fires → |
| 408 | // setSearchQuery fills cache → warm reports ~0ms while the user felt |
| 409 | // the real lag. |
| 410 | const [indexStatus, setIndexStatus] = React.useState<'building' | { |
| 411 | ms: number; |
| 412 | } | null>('building'); |
| 413 | React.useEffect(() => { |
| 414 | let alive = true; |
| 415 | const warm = jumpRef.current?.warmSearchIndex; |
| 416 | if (!warm) { |
| 417 | setIndexStatus(null); // VML not mounted yet — rare, skip indicator |
| 418 | return; |
| 419 | } |
| 420 | setIndexStatus('building'); |
| 421 | warm().then(ms => { |
| 422 | if (!alive) return; |
| 423 | // <20ms = imperceptible. No point showing "indexed in 3ms". |
| 424 | if (ms < 20) { |
| 425 | setIndexStatus(null); |
nothing calls this directly
no test coverage detected