1import { RegistryContext } from "@effect/atom-react";
2import { scopeProjectRef } from "@t3tools/client-runtime/environment";
3import { availableScratchWorkspaceRoot } from "@t3tools/client-runtime/operations/projects";
4import {
5 isAtomCommandInterrupted,
6 runAtomCommand,
7 squashAtomCommandFailure,
8} from "@t3tools/client-runtime/state/runtime";
9import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
10import type { EnvironmentId } from "@t3tools/contracts";
11import type { AtomRegistry } from "effect/reactivity";
12import { useContext } from "react";
13
14import { stackedThreadToast, toastManager } from "~/components/ui/toast";
15import {
16 type EnvironmentPresentation,
17 useEnvironments,
18 usePrimaryEnvironmentId,
19} from "~/state/environments";
20import { projectEnvironment } from "~/state/projects";
21import { type NewThreadHandler, useNewThreadHandler } from "./useHandleNewThread";
22
23function reportScratchFailure(title: string, error: unknown) {
24 toastManager.add(
25 stackedThreadToast({
26 type: "error",
27 title,
28 description: error instanceof Error ? error.message : "An error occurred.",
29 }),
30 );
31}
32
33type ScratchEnvironment = Pick<EnvironmentPresentation, "connection" | "serverConfig"> & {
34 readonly environmentId: EnvironmentId;
35};
36
37export function createScratchProjectActions(input: {
38 readonly environments: () => Iterable<ScratchEnvironment>;
39 readonly primaryEnvironmentId: () => EnvironmentId | null;
40 readonly registry: AtomRegistry.AtomRegistry;
41 readonly handleNewThread: NewThreadHandler;
42}) {
43 const scratchWorkspaceRootFor = (environmentId: EnvironmentId | null): string | null => {
44 let environment: ScratchEnvironment | undefined;
45 for (const entry of input.environments()) {
46 if (entry.environmentId === environmentId) environment = entry;
47 }
48 return availableScratchWorkspaceRoot(environment?.connection.phase, environment?.serverConfig);
49 };
50
51 // The primary (local) environment wins; `fallback` only applies when it offers no scratch root.
52 const scratchEnvironmentId = (fallback: EnvironmentId | null): EnvironmentId | null => {
53 const primary = input.primaryEnvironmentId();
54 if (primary !== null && scratchWorkspaceRootFor(primary) !== null) return primary;
55 if (fallback !== null && scratchWorkspaceRootFor(fallback) !== null) return fallback;
56 const offering = [...input.environments()].filter(
57 (entry) => scratchWorkspaceRootFor(entry.environmentId) !== null,
58 );
59 return offering.length === 1 ? (offering[0]?.environmentId ?? null) : null;
60 };
61
62 const openScratchProject = async (
63 environmentId: EnvironmentId,
64 failureTitle = "Could not start without a project",
65 ): Promise<EnvironmentProject | null> => {
66 const command = projectEnvironment.openScratch;
67 const result = await runAtomCommand(
68 input.registry,
69 command,
70 { environmentId, input: {} },
71 { label: command.label, reportFailure: false, reportDefect: true },
72 );
73 if (result._tag === "Success") return result.value;
74 if (!isAtomCommandInterrupted(result)) {
75 reportScratchFailure(failureTitle, squashAtomCommandFailure(result));
76 }
77 return null;
78 };
79
80 const startScratchThread = async (environmentId: EnvironmentId) => {
81 const project = await openScratchProject(environmentId);
82 if (project) {
83 await input
84 .handleNewThread(scopeProjectRef(project.environmentId, project.id))
85 .catch((error: unknown) =>
86 reportScratchFailure("Could not start without a project", error),
87 );
88 }
89 };
90
91 return { scratchWorkspaceRootFor, scratchEnvironmentId, openScratchProject, startScratchThread };
92}
93
94export function useScratchProject() {
95 const { environments } = useEnvironments();
96 const primaryEnvironmentId = usePrimaryEnvironmentId();
97 const registry = useContext(RegistryContext);
98 const handleNewThread = useNewThreadHandler();
99 return createScratchProjectActions({
100 environments: () => environments,
101 primaryEnvironmentId: () => primaryEnvironmentId,
102 registry,
103 handleNewThread,
104 });
105}