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;
})
| 423 | * useSearchInput handles readline editing; we report query changes and |
| 424 | * render the counter. Incremental — re-search + highlight per keystroke. */ |
| 425 | function TranscriptSearchBar({ |
| 426 | jumpRef, |
| 427 | count, |
| 428 | current, |
| 429 | onClose, |
| 430 | onCancel, |
| 431 | setHighlight, |
| 432 | initialQuery |
| 433 | }: { |
| 434 | jumpRef: RefObject<JumpHandle | null>; |
| 435 | count: number; |
| 436 | current: number; |
| 437 | /** Enter — commit. Query persists for n/N. */ |
| 438 | onClose: (lastQuery: string) => void; |
| 439 | /** Esc/ctrl+c/ctrl+g — undo to pre-/ state. */ |
| 440 | onCancel: () => void; |
| 441 | setHighlight: (query: string) => void; |
| 442 | // Seed with the previous query (less: / shows last pattern). Mount-fire |
| 443 | // of the effect re-scans with the same query — idempotent (same matches, |
| 444 | // nearest-ptr, same highlights). User can edit or clear. |
| 445 | initialQuery: string; |
| 446 | }): React.ReactNode { |
| 447 | const { |
| 448 | query, |
| 449 | cursorOffset |
| 450 | } = useSearchInput({ |
| 451 | isActive: true, |
| 452 | initialQuery, |
| 453 | onExit: () => onClose(query), |
| 454 | onCancel |
| 455 | }); |
| 456 | // Index warm-up runs before the query effect so it measures the real |
| 457 | // cost — otherwise setSearchQuery fills the cache first and warm |
| 458 | // reports ~0ms while the user felt the actual lag. |
| 459 | // First / in a transcript session pays the extractSearchText cost. |
| 460 | // Subsequent / return 0 immediately (indexWarmed ref in VML). |
| 461 | // Transcript is frozen at ctrl+o so the cache stays valid. |
| 462 | // Initial 'building' so warmDone is false on mount — the [query] effect |
| 463 | // waits for the warm effect's first resolve instead of racing it. With |
| 464 | // null initial, warmDone would be true on mount → [query] fires → |
| 465 | // setSearchQuery fills cache → warm reports ~0ms while the user felt |
| 466 | // the real lag. |
| 467 | const [indexStatus, setIndexStatus] = React.useState<'building' | { |
| 468 | ms: number; |
| 469 | } | null>('building'); |
| 470 | React.useEffect(() => { |
| 471 | let alive = true; |
| 472 | const warm = jumpRef.current?.warmSearchIndex; |
| 473 | if (!warm) { |
| 474 | setIndexStatus(null); // VML not mounted yet — rare, skip indicator |
| 475 | return; |
| 476 | } |
| 477 | setIndexStatus('building'); |
| 478 | warm().then(ms => { |
| 479 | if (!alive) return; |
| 480 | // <20ms = imperceptible. No point showing "indexed in 3ms". |
| 481 | if (ms < 20) { |
| 482 | setIndexStatus(null); |
nothing calls this directly
no test coverage detected