181 lines · 5.7 KB · TypeScript
RawDownload
import type { EnvironmentId, OrchestrationThread, ThreadId } from "@t3tools/contracts";import {  createThreadSearchResultsAtomFamily,  makeThreadSearchKey,  type EnvironmentThreadSearchMatch,} from "@t3tools/client-runtime/state/thread-search";import { useAtomValue } from "@effect/atom-react";import * as Option from "effect/Option";import { Atom } from "effect/unstable/reactivity";import { useEffect, useMemo, useState } from "react"; import { orchestrationEnvironment } from "./orchestration";import { projectEnvironment } from "./projects";import { useEnvironmentQuery } from "./query";import { useEnvironmentThread } from "./threads";import { vcsEnvironment } from "./vcs";import {  buildCheckpointDiffTargets,  normalizeComposerPathSearchQuery,  type CheckpointDiffTarget,} from "./queryTargets"; const COMPOSER_PATH_SEARCH_DEBOUNCE_MS = 200;const COMPOSER_PATH_SEARCH_LIMIT = 20;const THREAD_SEARCH_DEBOUNCE_MS = 200;const VCS_REF_LIST_LIMIT = 100;const EMPTY_THREAD_SEARCH_MATCHES: ReadonlyArray<EnvironmentThreadSearchMatch> = Object.freeze([]);const EMPTY_THREAD_SEARCH_ATOM = Atom.make({  matches: EMPTY_THREAD_SEARCH_MATCHES,  isLoading: false,}).pipe(Atom.withLabel("mobile:thread-search:empty")); const threadSearchResultsAtom = createThreadSearchResultsAtomFamily({  getSearchAtom: (environmentId, query) =>    orchestrationEnvironment.threadSearch({      environmentId,      input: { query },    }),  labelPrefix: "mobile:thread-search",}); export interface ThreadDetailView {  readonly data: OrchestrationThread | null;  readonly error: string | null;  readonly isPending: boolean;  readonly isDeleted: boolean;} export interface ComposerPathSearchTarget {  readonly environmentId: EnvironmentId | null;  readonly cwd: string | null;  readonly query: string | null;} function useDebouncedValue<A>(value: A, delayMs: number): A {  const [debounced, setDebounced] = useState(value);   useEffect(() => {    const timer = setTimeout(() => {      setDebounced(value);    }, delayMs);    return () => {      clearTimeout(timer);    };  }, [delayMs, value]);   return debounced;} export function useThreadSearch(  environmentIds: ReadonlyArray<EnvironmentId>,  query: string,): {  readonly matches: ReadonlyArray<EnvironmentThreadSearchMatch>;  readonly isPending: boolean;} {  const normalizedQuery = query.trim();  const debouncedQuery = useDebouncedValue(normalizedQuery, THREAD_SEARCH_DEBOUNCE_MS);  const canSearch = environmentIds.length > 0 && normalizedQuery.length >= 2;  const settledQuery = canSearch && normalizedQuery === debouncedQuery ? debouncedQuery : null;  const searchKey = useMemo(    () => (settledQuery === null ? null : makeThreadSearchKey(environmentIds, settledQuery)),    [environmentIds, settledQuery],  );  const result = useAtomValue(    searchKey === null ? EMPTY_THREAD_SEARCH_ATOM : threadSearchResultsAtom(searchKey),  );  const isDebouncing = canSearch && normalizedQuery !== debouncedQuery;  return {    matches: isDebouncing ? EMPTY_THREAD_SEARCH_MATCHES : result.matches,    isPending: canSearch && (isDebouncing || result.isLoading),  };} 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(input: {  readonly environmentId: EnvironmentId | null;  readonly cwd: string | null;  readonly query?: string | null;}) {  const query = input.query?.trim() ?? "";  return useEnvironmentQuery(    input.environmentId !== null && input.cwd !== null      ? vcsEnvironment.listRefs({          environmentId: input.environmentId,          input: {            cwd: input.cwd,            ...(query.length > 0 ? { query } : {}),            limit: VCS_REF_LIST_LIMIT,          },        })      : null,  );} export function useComposerPathSearch(target: ComposerPathSearchTarget) {  const normalizedTarget = useMemo(    () => ({      environmentId: target.environmentId,      cwd: target.cwd,      query: normalizeComposerPathSearchQuery(target.query),    }),    [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) {  const targets = useMemo(    () => buildCheckpointDiffTargets(target),    [      target.environmentId,      target.fromTurnCount,      target.ignoreWhitespace,      target.threadId,      target.toTurnCount,    ],  );  const fullThread = useEnvironmentQuery(    targets.fullThread === null      ? null      : orchestrationEnvironment.fullThreadDiff(targets.fullThread),  );  const turn = useEnvironmentQuery(    targets.turn === null ? null : orchestrationEnvironment.turnDiff(targets.turn),  );  return targets.fullThread === null ? turn : fullThread;}