1import { useAtomValue } from "@effect/atom-react";
2import { scopeThreadRef } from "@t3tools/client-runtime/environment";
3import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
4import { ProviderInstanceId, type ThreadId } from "@t3tools/contracts";
5import { useNavigate, useParams } from "@tanstack/react-router";
6import { useEffect, useEffectEvent, useState } from "react";
7
8import { newMessageId, newThreadId } from "~/lib/utils";
9import { useProjects, useThreadShell } from "../state/entities";
10import { usePrimaryEnvironmentId } from "../state/environments";
11import { primaryServerProvidersAtom } from "../state/server";
12import { threadEnvironment } from "../state/threads";
13import { useOrchestrationCommand } from "../state/use-orchestration-command";
14import { buildThreadRouteParams, resolveThreadRouteTarget } from "../threadRoutes";
15import { Button } from "./ui/button";
16import { Dialog, DialogHeader, DialogPanel, DialogPopup, DialogTitle } from "./ui/dialog";
17import { toastManager } from "./ui/toast";
18
19// Dev servers register this free ACP agent; see ProviderInstanceRegistryHydration.ts.
20const DEV_REPLAY_INSTANCE_ID = ProviderInstanceId.make("devReplay");
21
22/**
23 * Starts free replay threads on the primary environment in dev builds. Ctrl+Alt+Shift+R
24 * opens a picker, and `window.t3dev.replay(scenario)` does the same from the console.
25 */
26export function DevReplayHost() {
27 const [open, setOpen] = useState(false);
28 const navigate = useNavigate();
29 const environmentId = usePrimaryEnvironmentId();
30 const providers = useAtomValue(primaryServerProvidersAtom);
31 const projects = useProjects();
32 const target = resolveThreadRouteTarget(useParams({ strict: false }));
33 const routeThread = useThreadShell(target?.kind === "server" ? target.threadRef : null);
34 const startTurn = useOrchestrationCommand(threadEnvironment.startTurn, { reportFailure: false });
35
36 const replayProvider = providers.find(
37 (provider) => provider.instanceId === DEV_REPLAY_INSTANCE_ID && provider.enabled,
38 );
39 const scenarios = replayProvider?.models ?? [];
40 const environmentProjects = projects.filter((project) => project.environmentId === environmentId);
41 const project =
42 environmentProjects.find((candidate) => candidate.id === routeThread?.projectId) ??
43 environmentProjects[0];
44
45 const replay = async (scenario?: string): Promise<ThreadId> => {
46 const model = scenario ?? scenarios[0]?.slug;
47 if (environmentId === null || project === undefined || model === undefined) {
48 throw new Error("Replay needs a dev server with a project and the Replay (dev) provider.");
49 }
50 if (!scenarios.some((candidate) => candidate.slug === model)) {
51 throw new Error(`Unknown replay scenario "${model}".`);
52 }
53 const threadId = newThreadId();
54 const modelSelection = { instanceId: DEV_REPLAY_INSTANCE_ID, model };
55 const title = `Replay: ${model}`;
56 const createdAt = new Date().toISOString();
57 const result = await startTurn({
58 environmentId,
59 input: {
60 threadId,
61 message: { messageId: newMessageId(), role: "user", text: title, attachments: [] },
62 modelSelection,
63 runtimeMode: "approval-required",
64 interactionMode: "default",
65 bootstrap: {
66 createThread: {
67 projectId: project.id,
68 title,
69 modelSelection,
70 runtimeMode: "approval-required",
71 interactionMode: "default",
72 branch: null,
73 worktreePath: null,
74 createdAt,
75 },
76 },
77 createdAt,
78 },
79 });
80 if (result._tag === "Failure") throw squashAtomCommandFailure(result);
81 await navigate({
82 to: "/$environmentId/$threadId",
83 params: buildThreadRouteParams(scopeThreadRef(environmentId, threadId)),
84 });
85 return threadId;
86 };
87
88 const replayFromConsole = useEffectEvent(replay);
89 const listScenarios = useEffectEvent(() => scenarios.map((scenario) => scenario.slug));
90
91 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- exposes the console API on window and listens for the hidden window hotkey
92 useEffect(() => {
93 window.t3dev = {
94 replayScenarios: () => listScenarios(),
95 replay: (scenario) => replayFromConsole(scenario),
96 };
97 const onKeyDown = (event: KeyboardEvent) => {
98 if (event.repeat) return;
99 if (event.ctrlKey && event.altKey && event.shiftKey && event.code === "KeyR") {
100 event.preventDefault();
101 setOpen((current) => !current);
102 }
103 };
104 window.addEventListener("keydown", onKeyDown);
105 return () => {
106 delete window.t3dev;
107 window.removeEventListener("keydown", onKeyDown);
108 };
109 }, []);
110
111 const start = (scenario: string) => {
112 setOpen(false);
113 replay(scenario).catch((error: unknown) =>
114 toastManager.add({
115 type: "error",
116 title: "Replay failed",
117 description: error instanceof Error ? error.message : String(error),
118 }),
119 );
120 };
121
122 return (
123 <Dialog open={open} onOpenChange={setOpen}>
124 <DialogPopup className="sm:max-w-sm">
125 <DialogHeader>
126 <DialogTitle>
127 {project === undefined ? "Replay a recorded turn" : `Replay in ${project.title}`}
128 </DialogTitle>
129 </DialogHeader>
130 <DialogPanel>
131 {scenarios.length === 0 || project === undefined ? (
132 <p className="text-sm text-muted-foreground">
133 Needs a project and the Replay (dev) provider, which dev servers start.
134 </p>
135 ) : (
136 <div className="flex flex-col items-start gap-1">
137 {scenarios.map((scenario) => (
138 <Button key={scenario.slug} variant="ghost" onClick={() => start(scenario.slug)}>
139 {scenario.name}
140 </Button>
141 ))}
142 </div>
143 )}
144 </DialogPanel>
145 </DialogPopup>
146 </Dialog>
147 );
148}