Skip to content
Source
Sign inSign up

useStartTriage.ts

104 lines4.3 KB

1import { scopeProjectRef } from "@t3tools/client-runtime/environment";
2import {
3 type AtomCommandResult,
4 isAtomCommandInterrupted,
5 squashAtomCommandFailure,
6} from "@t3tools/client-runtime/state/runtime";
7import type { EnvironmentId } from "@t3tools/contracts";
8import { useRouter } from "@tanstack/react-router";
9​
10import { stackedThreadToast, toastManager } from "~/components/ui/toast";
11import { currentClientMetadata } from "~/connection/platform";
12import { projectEnvironment } from "~/state/projects";
13import { serverEnvironment } from "~/state/server";
14import { useAtomCommand } from "~/state/use-atom-command";
15import { writeTextToClipboard } from "./useCopyToClipboard";
16import { useNewThreadHandler } from "./useHandleNewThread";
17import { useScratchProject } from "./useScratchProject";
18​
19export const TRIAGE_CLI_COMMAND = "npx t3 triage";
20​
21const copyTriageCliCommand = () =>
22 writeTextToClipboard(TRIAGE_CLI_COMMAND, "command").catch(() =>
23 toastManager.add({
24 type: "error",
25 title: `Could not copy. Type ${TRIAGE_CLI_COMMAND} instead.`,
26 }),
27 );
28​
29/**
30 * In-app `t3 triage`: the server writes the session files and starts cloning
31 * the matching source, then a No project draft opens with the playbook linked.
32 * The draft hero explains the rest; the user picks a provider and describes
33 * the problem.
34 */
35export function useStartTriage() {
36 const router = useRouter();
37 const { scratchWorkspaceRootFor } = useScratchProject();
38 const handleNewThread = useNewThreadHandler();
39 const prepare = useAtomCommand(serverEnvironment.prepareTriage, { reportFailure: false });
40 const openScratch = useAtomCommand(projectEnvironment.openScratch, { reportFailure: false });
41​
42 // With no draft to show, a toast is the only place to point at the CLI.
43 const fail = (description: string) =>
44 toastManager.add(
45 stackedThreadToast({
46 type: "error",
47 title: "Could not start triage",
48 description: `${description} Run ${TRIAGE_CLI_COMMAND} in a terminal on that machine instead.`,
49 timeout: 0,
50 actionProps: { children: "Copy command", onClick: () => void copyTriageCliCommand() },
51 // "Copy command" is the copy that helps here, not the error text.
52 data: { hideCopyButton: true },
53 }),
54 );
55 const failWith = (
56 result: Extract<AtomCommandResult<unknown, unknown>, { readonly _tag: "Failure" }>,
57 ) => {
58 if (isAtomCommandInterrupted(result)) return;
59 const error = squashAtomCommandFailure(result);
60 fail(error instanceof Error ? error.message : "An error occurred.");
61 };
62​
63 // `routeChanged` tells the steps the user went elsewhere while they waited;
64 // the draft then stays closed instead of pulling them away.
65 const openTriageDraft = async (environmentId: EnvironmentId, routeChanged: () => boolean) => {
66 const prepared = await prepare({
67 environmentId,
68 input: { client: currentClientMetadata() },
69 });
70 if (prepared._tag === "Failure") return failWith(prepared);
71 const project = await openScratch({ environmentId, input: {} });
72 if (project._tag === "Failure") return failWith(project);
73 if (routeChanged()) return;
74 // A null result means the user went elsewhere; only a throw is a failure.
75 await handleNewThread(scopeProjectRef(project.value.environmentId, project.value.id), {
76 prompt: prepared.value.prompt,
77 }).catch(() => fail("T3 Code could not open a new thread."));
78 };
79​
80 return async (environmentId: EnvironmentId) => {
81 if (scratchWorkspaceRootFor(environmentId) === null) {
82 fail("This machine cannot start threads without a project.");
83 return;
84 }
85 const requestingRouteHref = router.state.location.href;
86 // The palette is already closed, and a slow network can hold up the source
87 // lookup, so say something when the draft does not open at once.
88 let pendingToastId: ReturnType<typeof toastManager.add> | undefined;
89 const pendingTimer = setTimeout(() => {
90 pendingToastId = toastManager.add({
91 type: "loading",
92 title: "Starting triage…",
93 timeout: 0,
94 });
95 }, 400);
96 await openTriageDraft(
97 environmentId,
98 () => router.state.location.href !== requestingRouteHref,
99 ).finally(() => {
100 clearTimeout(pendingTimer);
101 if (pendingToastId !== undefined) toastManager.close(pendingToastId);
102 });
103 };