import { useCallback } from "react"; import { useAtomSet, useAtomValue } from "@effect/atom-react"; import { Data, Effect, Option, Schema } from "effect"; import * as Atom from "effect/unstable/reactivity/Atom"; import { createFileRoute } from "@tanstack/react-router"; import { ResumeApprovalPage, ResumeApprovalPageView } from "../pages/resume-approval"; import { getExecutorServerAuthorizationHeader } from "../api/server-connection"; import type { ElicitationAction } from "../components/elicitation-approval"; import { useExecutorDocumentTitle } from "../lib/document-title"; const SearchParams = Schema.toStandardSchemaV1( Schema.Struct({ mcp_session_id: Schema.optional(Schema.String), }), ); const LocalMcpResumeCompleted = Schema.Struct({ status: Schema.Literal("completed"), text: Schema.String, structured: Schema.Unknown, isError: Schema.Boolean, }); const LocalMcpResumePaused = Schema.Struct({ status: Schema.Literal("paused"), text: Schema.String, structured: Schema.Unknown, }); const LocalMcpResumeResult = Schema.Union([LocalMcpResumeCompleted, LocalMcpResumePaused]); const decodeLocalMcpResumeResult = Schema.decodeUnknownOption(LocalMcpResumeResult); class LocalMcpResumeError extends Data.TaggedError("LocalMcpResumeError")<{ readonly message: string; }> {} const McpPausedExecutionInfo = Schema.Struct({ text: Schema.String, structured: Schema.Unknown, }); const decodeMcpPausedExecutionInfo = Schema.decodeUnknownOption(McpPausedExecutionInfo); // Paused-execution detail for the in-process / Cloudflare hosts, fetched // session-scoped: the MCP paused execution lives in its session's engine, so it // is resolved through `/api/mcp-sessions/:id/...`, not the session-less // `/api/executions/:id` (which hits a different engine and can't see it). Cloud // serves the equivalent through its own route + Durable Object RPC. const mcpPausedExecutionAtom = Atom.family( (key: { readonly mcpSessionId: string; readonly executionId: string }) => Atom.make( Effect.gen(function* () { // `/api/mcp-sessions/*` is bearer-gated. Attach the bearer (standalone // web reads it from localStorage; desktop injects it at the session // layer, so this is null and we send none) — otherwise this 401s on the // single-user local server and the page shows "unavailable". const authorization = getExecutorServerAuthorizationHeader(); const response = yield* Effect.tryPromise({ try: () => fetch( `/api/mcp-sessions/${encodeURIComponent(key.mcpSessionId)}/executions/${encodeURIComponent(key.executionId)}`, authorization ? { headers: { authorization } } : undefined, ), catch: () => new LocalMcpResumeError({ message: "Failed to load the paused execution." }), }); if (!response.ok) { return yield* new LocalMcpResumeError({ message: `Paused execution unavailable (${response.status}).`, }); } const body = yield* Effect.tryPromise({ try: () => response.json(), catch: () => new LocalMcpResumeError({ message: "Paused response was not valid JSON." }), }); const decoded = decodeMcpPausedExecutionInfo(body); if (Option.isNone(decoded)) { return yield* new LocalMcpResumeError({ message: "Paused response had an unexpected shape.", }); } return decoded.value; }), ), ); type LocalMcpResumeInput = { readonly mcpSessionId: string; readonly executionId: string; readonly action: ElicitationAction; readonly content?: Record; }; const resumeLocalMcpExecution = Atom.fn()((input) => Effect.gen(function* () { // `/api/mcp-sessions/*` is bearer-gated like the rest of /api. Attach the // bearer the same way the typed client does (standalone web reads it from // localStorage; on desktop the connection carries no auth and the main // process injects the header, so this is null and we send none). const authorization = getExecutorServerAuthorizationHeader(); const response = yield* Effect.tryPromise({ try: () => fetch( `/api/mcp-sessions/${encodeURIComponent(input.mcpSessionId)}/executions/${encodeURIComponent(input.executionId)}/resume`, { method: "POST", headers: { "content-type": "application/json", ...(authorization ? { authorization } : {}), }, body: JSON.stringify( input.action === "accept" ? { action: input.action, content: input.content ?? {} } : { action: input.action }, ), }, ), catch: () => new LocalMcpResumeError({ message: "Failed to submit approval." }), }); if (!response.ok) { const body = yield* Effect.tryPromise({ try: () => response.text(), catch: () => "", }).pipe(Effect.orElseSucceed(() => "")); return yield* new LocalMcpResumeError({ message: body || `Approval request failed (${response.status}).`, }); } const body = yield* Effect.tryPromise({ try: () => response.json(), catch: () => new LocalMcpResumeError({ message: "Approval response was not valid JSON." }), }); const result = decodeLocalMcpResumeResult(body); if (Option.isNone(result)) { return yield* new LocalMcpResumeError({ message: "Approval response had an unexpected shape.", }); } return result.value; }), ); export const Route = createFileRoute("/{-$orgSlug}/resume/$executionId")({ validateSearch: SearchParams, component: RouteComponent, }); function RouteComponent() { useExecutorDocumentTitle("Resume"); const { executionId } = Route.useParams(); const { mcp_session_id: mcpSessionId } = Route.useSearch(); if (mcpSessionId) { return ; } return ; } function LocalMcpResumeApproval(props: { executionId: string; mcpSessionId: string }) { const paused = useAtomValue( mcpPausedExecutionAtom({ mcpSessionId: props.mcpSessionId, executionId: props.executionId }), ); const doResume = useAtomSet(resumeLocalMcpExecution, { mode: "promiseExit" }); const resume = useCallback( (executionId: string, action: ElicitationAction, content?: Record) => doResume({ mcpSessionId: props.mcpSessionId, executionId, action, content, }), [doResume, props.mcpSessionId], ); return ( ); }