1import * as Effect from "effect/Effect";
2import * as Preview from "../../../preview/Manager.ts";
3import { requireThreadMcpCapability } from "../../McpInvocationContext.ts";
4import * as McpToolAccess from "../../McpToolAccess.ts";
5import { unavailable } from "../../threadAccess.ts";
6import { PreviewControlsToolkit } from "./tools.ts";
7import * as PreviewAutomationBroker from "../../PreviewAutomationBroker.ts";
8
9const access = Effect.gen(function* () {
10 // The preview capability already reflects the calling project's access setting.
11 const scope = yield* requireThreadMcpCapability("preview");
12 return { scope, manager: yield* Preview.PreviewManager };
13});
14export const layer = McpToolAccess.toLayer(PreviewControlsToolkit, {
15 t3_preview_list: McpToolAccess.readsAsCaller((input) =>
16 Effect.gen(function* () {
17 const { scope, manager } = yield* access;
18 const result = yield* manager.list({ threadId: scope.thread.threadId });
19 const start = input.cursor ?? 0;
20 const end = start + (input.limit ?? 20);
21 return {
22 ...result,
23 sessions: result.sessions.slice(start, end),
24 nextCursor: end < result.sessions.length ? end : null,
25 };
26 }),
27 ),
28 t3_preview_close: McpToolAccess.actsAsCaller((input) =>
29 Effect.gen(function* () {
30 const { scope, manager } = yield* access;
31 const { sessions } = yield* manager.list({ threadId: scope.thread.threadId });
32 if (
33 sessions.some((session) => session.tabId === input.tabId && session.runtime === "server")
34 ) {
35 const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;
36 yield* broker
37 .invoke({ scope, operation: "close", input: {}, tabId: input.tabId })
38 .pipe(Effect.mapError(unavailable));
39 return {};
40 }
41 yield* manager
42 .close({ threadId: scope.thread.threadId, tabId: input.tabId })
43 .pipe(Effect.mapError(unavailable));
44 return {};
45 }),
46 ),
47});