Skip to content
Source
Sign inSign up
Open History to see the last commit

DevReplayHost.tsx

150 lines5.9 KB

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