1import type { OrchestrationV2SearchThreadInput, RunId } from "@t3tools/contracts";
2
3export type ThreadFindStart = NonNullable<OrchestrationV2SearchThreadInput["start"]>;
4export type ThreadFindPositionReader = (query: string) => ThreadFindStart | undefined;
5
6/** One occurrence of the query inside a searchable timeline entry. */
7export 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
15function 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
20export 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
26export function formatThreadFindCount(index: number, total: number): string {
27 return total <= 0 ? "0/0" : `${clampThreadFindIndex(index, total) + 1}/${total}`;
28}