| 1 | import type { OrchestrationV2SearchThreadInput, RunId } from "@t3tools/contracts"; |
| 2 | |
| 3 | export type ThreadFindStart = NonNullable<OrchestrationV2SearchThreadInput["start"]>; |
| 4 | export type ThreadFindPositionReader = (query: string) => ThreadFindStart | undefined; |
| 5 | |
| 6 | /** One occurrence of the query inside a searchable timeline entry. */ |
| 7 | export interface ThreadFindMatch { |
| 8 | readonly entryId: string; |
| 9 | /** The turn to expand when the matching entry is folded. */ |
| 10 | readonly runId: RunId | null; |
| 11 | /** Zero-based occurrence within this timeline entry. */ |
| 12 | readonly occurrence: number; |
| 13 | } |
| 14 | |
| 15 | function clampThreadFindIndex(index: number, total: number): number { |
| 16 | if (total <= 0 || !Number.isFinite(index) || index < 0) return 0; |
| 17 | return Math.min(Math.trunc(index), total - 1); |
| 18 | } |
| 19 | |
| 20 | export function stepThreadFindIndex(index: number, total: number, delta: number): number { |
| 21 | if (total <= 0) return 0; |
| 22 | const clamped = clampThreadFindIndex(index, total); |
| 23 | return (((clamped + delta) % total) + total) % total; |
| 24 | } |
| 25 | |
| 26 | export function formatThreadFindCount(index: number, total: number): string { |
| 27 | return total <= 0 ? "0/0" : `${clampThreadFindIndex(index, total) + 1}/${total}`; |
| 28 | } |