import { useAtomRefresh, useAtomValue } from "@effect/atom-react";import type { EnvironmentId, ProjectListEntriesResult, ProjectReadFileResult,} from "@t3tools/contracts";import { isWorkspaceImagePreviewPath, isWorkspaceVideoPreviewPath,} from "@t3tools/shared/filePreview";import * as Cause from "effect/Cause";import * as Option from "effect/Option";import { AsyncResult, Atom } from "effect/unstable/reactivity";import { useCallback } from "react"; import { appAtomRegistry } from "~/rpc/atomRegistry";import { projectEnvironment } from "~/state/projects";import { useProjectPathSearch } from "~/state/queries";import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; const EMPTY_PROJECT_FILE_PATH = "";const EMPTY_PROJECT_FILE_QUERY_ATOM = Atom.make( AsyncResult.initial<ProjectReadFileResult, never>(false),).pipe(Atom.withLabel("project-file-query:empty"));function optimisticFileAtom(environmentId: EnvironmentId, cwd: string, relativePath: string) { return projectEnvironment.optimisticFile({ environmentId, cwd, relativePath });} interface ProjectQueryState<A> { readonly data: A | null; readonly error: string | null; readonly isPending: boolean; readonly refresh: () => void;} function getProjectEntriesQueryAtom(environmentId: EnvironmentId, cwd: string) { return projectEnvironment.listEntries({ environmentId, input: { cwd } });} export function getProjectFileQueryAtom( environmentId: EnvironmentId, cwd: string, relativePath: string | null,) { return projectEnvironment.readFile({ environmentId, input: { cwd, relativePath: relativePath ?? EMPTY_PROJECT_FILE_PATH }, });} export function setProjectFileQueryData( environmentId: EnvironmentId, cwd: string, relativePath: string, contents: string,): void { appAtomRegistry.set(optimisticFileAtom(environmentId, cwd, relativePath), { confirmedAgainst: undefined, data: { relativePath, contents, byteLength: new TextEncoder().encode(contents).byteLength, truncated: false, }, });} export function getOptimisticProjectFileQueryData( environmentId: EnvironmentId, cwd: string, relativePath: string,): ProjectReadFileResult | null { return appAtomRegistry.get(optimisticFileAtom(environmentId, cwd, relativePath))?.data ?? null;} export function confirmProjectFileQueryData( environmentId: EnvironmentId, cwd: string, relativePath: string, contents: string,): boolean { const atom = optimisticFileAtom(environmentId, cwd, relativePath); const optimisticFile = appAtomRegistry.get(atom); if (optimisticFile?.data.contents !== contents) return false; const queryAtom = getProjectFileQueryAtom(environmentId, cwd, relativePath); const confirmed = { ...optimisticFile, confirmedAgainst: appAtomRegistry.get(queryAtom), }; appAtomRegistry.set(atom, confirmed); appAtomRegistry.refresh(queryAtom); void executeAtomQuery(appAtomRegistry, queryAtom, { reportDefect: false, reportFailure: false, }).then((result) => { if (result._tag === "Success" && appAtomRegistry.get(atom) === confirmed) { appAtomRegistry.set(atom, null); } }); return true;} export function resolveProjectFileQueryData( environmentId: EnvironmentId, cwd: string, relativePath: string | null, data: ProjectReadFileResult | null,): ProjectReadFileResult | null { if (relativePath === null) return data; return appAtomRegistry.get(optimisticFileAtom(environmentId, cwd, relativePath))?.data ?? data;} export function clearProjectFileQueryData( environmentId: EnvironmentId, cwd: string, relativePath: string,): void { appAtomRegistry.set(optimisticFileAtom(environmentId, cwd, relativePath), null);} function errorMessage<A>(result: AsyncResult.AsyncResult<A, unknown>): string | null { if (result._tag !== "Failure") return null; const cause = Cause.squash(result.cause); return cause instanceof Error ? cause.message : "Workspace query failed.";} export function useProjectEntriesQuery( environmentId: EnvironmentId, cwd: string,): ProjectQueryState<ProjectListEntriesResult> { const atom = getProjectEntriesQueryAtom(environmentId, cwd); const result = useAtomValue(atom); const refreshAtom = useAtomRefresh(atom); const refresh = useCallback(() => refreshAtom(), [refreshAtom]); return { data: Option.getOrNull(AsyncResult.value(result)), error: errorMessage(result), isPending: result.waiting, refresh, };} /** * Backing query for the project file picker: a debounced, bounded, file-only * server search. An empty query is a valid request — the index answers it * with frecency-ordered files, so the picker's initial view is recent files * without transferring the full workspace listing. `matchedQuery` is the * query the returned entries were computed for, so the caller can highlight * against results instead of half-typed input. */export function useProjectFilePickerQuery( environmentId: EnvironmentId, cwd: string, query: string, limit: number, options?: { readonly imageOnly?: boolean },) { const search = useProjectPathSearch( { environmentId, cwd, query, kind: "file", ...(options?.imageOnly ? { imageOnly: true } : {}), }, limit, { allowEmptyQuery: true }, ); return { entries: search.isPending ? [] : search.entries, error: search.error, isPending: search.isPending, matchedQuery: search.searchedQuery, };} export function useProjectFileQuery( environmentId: EnvironmentId, cwd: string, relativePath: string | null, enabled = true,): ProjectQueryState<ProjectReadFileResult> { const isMedia = relativePath !== null && (isWorkspaceImagePreviewPath(relativePath) || isWorkspaceVideoPreviewPath(relativePath)); const atom = enabled && !isMedia ? getProjectFileQueryAtom(environmentId, cwd, relativePath) : EMPTY_PROJECT_FILE_QUERY_ATOM; const result = useAtomValue(atom); const refreshAtom = useAtomRefresh(atom); const refresh = useCallback(() => refreshAtom(), [refreshAtom]); const data = Option.getOrNull(AsyncResult.value(result)); const optimisticResult = useAtomValue( optimisticFileAtom(environmentId, cwd, relativePath ?? EMPTY_PROJECT_FILE_PATH), ); const optimisticFile = relativePath === null ? null : optimisticResult; return { data: optimisticFile?.data ?? data, error: errorMessage(result), isPending: result.waiting, refresh, };}
import { useAtomRefresh, useAtomValue } from "@effect/atom-react";
import type {
EnvironmentId,
ProjectListEntriesResult,
ProjectReadFileResult,
} from "@t3tools/contracts";
import {
isWorkspaceImagePreviewPath,
isWorkspaceVideoPreviewPath,
} from "@t3tools/shared/filePreview";
import * as Cause from "effect/Cause";
import * as Option from "effect/Option";
import { AsyncResult, Atom } from "effect/unstable/reactivity";
import { useCallback } from "react";
import { appAtomRegistry } from "~/rpc/atomRegistry";
import { projectEnvironment } from "~/state/projects";
import { useProjectPathSearch } from "~/state/queries";
import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime";
const EMPTY_PROJECT_FILE_PATH = "";
const EMPTY_PROJECT_FILE_QUERY_ATOM = Atom.make(
AsyncResult.initial<ProjectReadFileResult, never>(false),
).pipe(Atom.withLabel("project-file-query:empty"));
function optimisticFileAtom(environmentId: EnvironmentId, cwd: string, relativePath: string) {
return projectEnvironment.optimisticFile({ environmentId, cwd, relativePath });
}
interface ProjectQueryState<A> {
readonly data: A | null;
readonly error: string | null;
readonly isPending: boolean;
readonly refresh: () => void;
}
function getProjectEntriesQueryAtom(environmentId: EnvironmentId, cwd: string) {
return projectEnvironment.listEntries({ environmentId, input: { cwd } });
}
export function getProjectFileQueryAtom(
environmentId: EnvironmentId,
cwd: string,
relativePath: string | null,
) {
return projectEnvironment.readFile({
environmentId,
input: { cwd, relativePath: relativePath ?? EMPTY_PROJECT_FILE_PATH },
});
}
export function setProjectFileQueryData(
environmentId: EnvironmentId,
cwd: string,
relativePath: string,
contents: string,
): void {
appAtomRegistry.set(optimisticFileAtom(environmentId, cwd, relativePath), {
confirmedAgainst: undefined,
data: {
relativePath,
contents,
byteLength: new TextEncoder().encode(contents).byteLength,
truncated: false,
},
});
}
export function getOptimisticProjectFileQueryData(
environmentId: EnvironmentId,
cwd: string,
relativePath: string,
): ProjectReadFileResult | null {
return appAtomRegistry.get(optimisticFileAtom(environmentId, cwd, relativePath))?.data ?? null;
}
export function confirmProjectFileQueryData(
environmentId: EnvironmentId,
cwd: string,
relativePath: string,
contents: string,
): boolean {
const atom = optimisticFileAtom(environmentId, cwd, relativePath);
const optimisticFile = appAtomRegistry.get(atom);
if (optimisticFile?.data.contents !== contents) return false;
const queryAtom = getProjectFileQueryAtom(environmentId, cwd, relativePath);
const confirmed = {
...optimisticFile,
confirmedAgainst: appAtomRegistry.get(queryAtom),
};
appAtomRegistry.set(atom, confirmed);
appAtomRegistry.refresh(queryAtom);
void executeAtomQuery(appAtomRegistry, queryAtom, {
reportDefect: false,
reportFailure: false,
}).then((result) => {
if (result._tag === "Success" && appAtomRegistry.get(atom) === confirmed) {
appAtomRegistry.set(atom, null);
}
});
return true;
}
export function resolveProjectFileQueryData(
environmentId: EnvironmentId,
cwd: string,
relativePath: string | null,
data: ProjectReadFileResult | null,
): ProjectReadFileResult | null {
if (relativePath === null) return data;
return appAtomRegistry.get(optimisticFileAtom(environmentId, cwd, relativePath))?.data ?? data;
}
export function clearProjectFileQueryData(
environmentId: EnvironmentId,
cwd: string,
relativePath: string,
): void {
appAtomRegistry.set(optimisticFileAtom(environmentId, cwd, relativePath), null);
}
function errorMessage<A>(result: AsyncResult.AsyncResult<A, unknown>): string | null {
if (result._tag !== "Failure") return null;
const cause = Cause.squash(result.cause);
return cause instanceof Error ? cause.message : "Workspace query failed.";
}
export function useProjectEntriesQuery(
environmentId: EnvironmentId,
cwd: string,
): ProjectQueryState<ProjectListEntriesResult> {
const atom = getProjectEntriesQueryAtom(environmentId, cwd);
const result = useAtomValue(atom);
const refreshAtom = useAtomRefresh(atom);
const refresh = useCallback(() => refreshAtom(), [refreshAtom]);
return {
data: Option.getOrNull(AsyncResult.value(result)),
error: errorMessage(result),
isPending: result.waiting,
refresh,
};
}
/**
* Backing query for the project file picker: a debounced, bounded, file-only
* server search. An empty query is a valid request — the index answers it
* with frecency-ordered files, so the picker's initial view is recent files
* without transferring the full workspace listing. `matchedQuery` is the
* query the returned entries were computed for, so the caller can highlight
* against results instead of half-typed input.
*/
export function useProjectFilePickerQuery(
environmentId: EnvironmentId,
cwd: string,
query: string,
limit: number,
options?: { readonly imageOnly?: boolean },
) {
const search = useProjectPathSearch(
{
environmentId,
cwd,
query,
kind: "file",
...(options?.imageOnly ? { imageOnly: true } : {}),
},
limit,
{ allowEmptyQuery: true },
);
return {
entries: search.isPending ? [] : search.entries,
error: search.error,
isPending: search.isPending,
matchedQuery: search.searchedQuery,
};
}
export function useProjectFileQuery(
environmentId: EnvironmentId,
cwd: string,
relativePath: string | null,
enabled = true,
): ProjectQueryState<ProjectReadFileResult> {
const isMedia =
relativePath !== null &&
(isWorkspaceImagePreviewPath(relativePath) || isWorkspaceVideoPreviewPath(relativePath));
const atom =
enabled && !isMedia
? getProjectFileQueryAtom(environmentId, cwd, relativePath)
: EMPTY_PROJECT_FILE_QUERY_ATOM;
const result = useAtomValue(atom);
const refreshAtom = useAtomRefresh(atom);
const refresh = useCallback(() => refreshAtom(), [refreshAtom]);
const data = Option.getOrNull(AsyncResult.value(result));
const optimisticResult = useAtomValue(
optimisticFileAtom(environmentId, cwd, relativePath ?? EMPTY_PROJECT_FILE_PATH),
);
const optimisticFile = relativePath === null ? null : optimisticResult;
return {
data: optimisticFile?.data ?? data,
error: errorMessage(result),
isPending: result.waiting,
refresh,
};
}