import { useAtomValue } from "@effect/atom-react"; import { type CheckpointDiffTarget, type ComposerPathSearchTarget, } from "@t3tools/client-runtime/state/threads"; import { type VcsRefTarget } from "@t3tools/client-runtime/state/vcs"; import type { EnvironmentId, OrchestrationThread, ThreadId, VcsListRefsResult, VcsRef, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Option from "effect/Option"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { useCallback, useEffect, useMemo, useState } from "react"; import { appAtomRegistry } from "../rpc/atomRegistry"; import { orchestrationEnvironment } from "./orchestration"; import { projectEnvironment } from "./projects"; import { useEnvironmentQuery } from "./query"; import { useEnvironmentThread } from "./threads"; import { vcsEnvironment } from "./vcs"; const COMPOSER_PATH_SEARCH_DEBOUNCE_MS = 120; const COMPOSER_PATH_SEARCH_LIMIT = 80; const VCS_REF_LIST_LIMIT = 100; const EMPTY_REFS: ReadonlyArray = []; const INITIAL_BRANCH_CURSORS = [undefined] as const; export interface ThreadDetailView { readonly data: OrchestrationThread | null; readonly error: string | null; readonly isPending: boolean; readonly isDeleted: boolean; } function useDebouncedValue(value: A, delayMs: number): A { const [debounced, setDebounced] = useState(value); useEffect(() => { const timer = window.setTimeout(() => { setDebounced(value); }, delayMs); return () => { window.clearTimeout(timer); }; }, [delayMs, value]); return debounced; } export function useThreadDetail( environmentId: EnvironmentId | null, threadId: ThreadId | null, ): ThreadDetailView { const state = useEnvironmentThread(environmentId, threadId); return { data: Option.getOrNull(state.data), error: Option.getOrNull(state.error), isPending: state.status === "synchronizing", isDeleted: state.status === "deleted", }; } export function useBranches(target: VcsRefTarget) { const query = target.query?.trim() ?? ""; return useEnvironmentQuery( target.environmentId !== null && target.cwd !== null ? vcsEnvironment.listRefs({ environmentId: target.environmentId, input: { cwd: target.cwd, ...(query.length > 0 ? { query } : {}), limit: VCS_REF_LIST_LIMIT, }, }) : null, ); } export function usePaginatedBranches(target: VcsRefTarget) { const query = target.query?.trim() ?? ""; const targetKey = target.environmentId !== null && target.cwd !== null ? JSON.stringify([target.environmentId, target.cwd, query]) : null; const [pagination, setPagination] = useState<{ readonly targetKey: string | null; readonly cursors: ReadonlyArray; }>({ targetKey, cursors: INITIAL_BRANCH_CURSORS, }); const cursors = pagination.targetKey === targetKey ? pagination.cursors : INITIAL_BRANCH_CURSORS; const pageAtoms = useMemo( () => target.environmentId !== null && target.cwd !== null ? cursors.map((cursor) => vcsEnvironment.listRefs({ environmentId: target.environmentId!, input: { cwd: target.cwd!, ...(query.length > 0 ? { query } : {}), ...(cursor === undefined ? {} : { cursor }), limit: VCS_REF_LIST_LIMIT, }, }), ) : [], [cursors, query, target.cwd, target.environmentId], ); const pagesAtom = useMemo( () => Atom.make((get) => pageAtoms.map((atom) => get(atom))).pipe( Atom.withLabel(`web:vcs-ref-pages:${targetKey ?? "empty"}`), ), [pageAtoms, targetKey], ); const results = useAtomValue(pagesAtom); const values = results.flatMap((result) => { const value = Option.getOrNull(AsyncResult.value(result)); return value === null ? [] : [value]; }); const refs = new Map(); for (const value of values) { for (const ref of value.refs) { refs.set(ref.name, ref); } } const first = values[0] ?? null; const last = values.at(-1) ?? null; const data: VcsListRefsResult | null = first === null || last === null ? null : { refs: [...refs.values()], isRepo: first.isRepo, hasPrimaryRemote: first.hasPrimaryRemote, nextCursor: last.nextCursor, totalCount: Math.max(...values.map((value) => value.totalCount)), }; const failed = results.find((result) => result._tag === "Failure"); const error = failed?._tag === "Failure" ? (() => { const cause = Cause.squash(failed.cause); return cause instanceof Error && cause.message.trim().length > 0 ? cause.message : "Failed to load refs."; })() : null; const refresh = useCallback(() => { const firstPage = pageAtoms[0]; setPagination({ targetKey, cursors: INITIAL_BRANCH_CURSORS }); if (firstPage !== undefined) { appAtomRegistry.refresh(firstPage); } }, [pageAtoms, targetKey]); const loadNext = useCallback(() => { if (targetKey === null || data?.nextCursor === null || data?.nextCursor === undefined) { return; } setPagination((current) => { const currentCursors = current.targetKey === targetKey ? current.cursors : INITIAL_BRANCH_CURSORS; return currentCursors.includes(data.nextCursor!) ? { targetKey, cursors: currentCursors } : { targetKey, cursors: [...currentCursors, data.nextCursor!] }; }); }, [data?.nextCursor, targetKey]); return { data, refs: data?.refs ?? EMPTY_REFS, error, isPending: results.some((result) => result.waiting), refresh, loadNext, }; } export function useComposerPathSearch(target: ComposerPathSearchTarget) { const normalizedTarget = useMemo( () => ({ environmentId: target.environmentId, cwd: target.cwd, query: target.query?.trim() ?? "", }), [target.cwd, target.environmentId, target.query], ); const debouncedTarget = useDebouncedValue(normalizedTarget, COMPOSER_PATH_SEARCH_DEBOUNCE_MS); const result = useEnvironmentQuery( debouncedTarget.environmentId !== null && debouncedTarget.cwd !== null && debouncedTarget.query.length > 0 ? projectEnvironment.searchEntries({ environmentId: debouncedTarget.environmentId, input: { cwd: debouncedTarget.cwd, query: debouncedTarget.query, limit: COMPOSER_PATH_SEARCH_LIMIT, }, }) : null, ); return { entries: result.data?.entries ?? [], error: result.error, isPending: normalizedTarget.query !== debouncedTarget.query || result.isPending, refresh: result.refresh, }; } export function useCheckpointDiff( target: CheckpointDiffTarget, options?: { readonly enabled?: boolean }, ) { const enabled = options?.enabled !== false && target.environmentId !== null && target.threadId !== null && target.fromTurnCount !== null && target.toTurnCount !== null; const fullThreadTarget = enabled && target.fromTurnCount === 0 ? { environmentId: target.environmentId!, input: { threadId: target.threadId!, toTurnCount: target.toTurnCount!, ignoreWhitespace: target.ignoreWhitespace, }, } : null; const turnTarget = enabled && target.fromTurnCount !== 0 ? { environmentId: target.environmentId!, input: { threadId: target.threadId!, fromTurnCount: target.fromTurnCount!, toTurnCount: target.toTurnCount!, ignoreWhitespace: target.ignoreWhitespace, }, } : null; const fullThread = useEnvironmentQuery( fullThreadTarget === null ? null : orchestrationEnvironment.fullThreadDiff(fullThreadTarget), ); const turn = useEnvironmentQuery( turnTarget === null ? null : orchestrationEnvironment.turnDiff(turnTarget), ); return fullThreadTarget === null ? turn : fullThread; }