1import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
2import type { DesktopAppActivationRequest } from "@t3tools/contracts";
3import { useEffect, useEffectEvent, useRef } from "react";
4
5import { handleDesktopAppActivationRequest } from "../../desktopAppActivation";
6import { useNewThreadHandler } from "../../hooks/useHandleNewThread";
7import { findProjectByPath, inferProjectTitleFromPath } from "../../lib/projectPaths";
8import { newProjectId } from "../../lib/utils";
9import { readProjects, waitForProject } from "../../state/entities";
10import { usePrimaryEnvironment } from "../../state/environments";
11import { projectEnvironment } from "../../state/projects";
12import { useEnvironmentQuery } from "../../state/query";
13import { environmentShell } from "../../state/shell";
14import { useAtomCommand } from "../../state/use-atom-command";
15
16export function DesktopAppActivationCoordinator() {
17 const primaryEnvironment = usePrimaryEnvironment();
18 const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false });
19 const openThread = useNewThreadHandler();
20 const queueRef = useRef(Promise.resolve());
21 const activation = window.desktopBridge?.appActivation;
22 const shell = useEnvironmentQuery(
23 primaryEnvironment === null
24 ? null
25 : environmentShell.stateAtom(primaryEnvironment.environmentId),
26 );
27 const ready =
28 activation !== undefined &&
29 primaryEnvironment?.connection.phase === "connected" &&
30 primaryEnvironment.serverConfig !== null &&
31 shell.data?.snapshot._tag === "Some";
32
33 const processRequest = useEffectEvent(async (request: DesktopAppActivationRequest) =>
34 handleDesktopAppActivationRequest(request, {
35 getTarget: () => {
36 if (
37 primaryEnvironment?.connection.phase !== "connected" ||
38 primaryEnvironment.serverConfig === null
39 ) {
40 return null;
41 }
42 return {
43 environmentId: primaryEnvironment.environmentId,
44 platform: primaryEnvironment.serverConfig.environment.platform.os,
45 };
46 },
47 findProject: (environmentId, workspaceRoot) =>
48 findProjectByPath(
49 readProjects().filter((project) => project.environmentId === environmentId),
50 workspaceRoot,
51 ) ?? null,
52 createProject: async (environmentId, workspaceRoot) => {
53 const projectId = newProjectId();
54 const result = await createProject({
55 environmentId,
56 input: {
57 projectId,
58 title: inferProjectTitleFromPath(workspaceRoot),
59 workspaceRoot,
60 createWorkspaceRootIfMissing: false,
61 defaultModelSelection: null,
62 },
63 });
64 if (result._tag === "Failure") {
65 const error = squashAtomCommandFailure(result);
66 throw error instanceof Error ? error : new Error("The Knife could not add the project.");
67 }
68 return projectId;
69 },
70 waitForProject: async (projectRef) => {
71 await waitForProject(projectRef);
72 },
73 openThread: (projectRef) => openThread(projectRef),
74 }),
75 );
76
77 useEffect(() => {
78 if (!ready || activation === undefined) return;
79
80 let subscribed = true;
81 const unsubscribe = activation.onRequest((request) => {
82 queueRef.current = queueRef.current.then(async () => {
83 const response = await processRequest(request);
84 await activation.complete(response);
85 });
86 queueRef.current = queueRef.current.catch(() => undefined);
87 });
88 // Skip readiness if React runs cleanup before this subscription can receive requests.
89 queueMicrotask(() => {
90 if (subscribed) void activation.setReady(true).catch(() => undefined);
91 });
92 return () => {
93 subscribed = false;
94 void activation.setReady(false).catch(() => undefined);
95 unsubscribe();
96 };
97 }, [activation, ready]);
98
99 return null;
100}