1import { OrchestratorMcpFailure } from "@t3tools/contracts";
2import {
3 HTML_RENDER_COLUMN_WIDTH,
4 HTML_RENDER_LAYOUT_GUIDE,
5 HTML_RENDER_MAX_HEIGHT,
6 HTML_RENDER_MAX_TITLE_LENGTH,
7 HTML_RENDER_MIN_HEIGHT,
8 HTML_RENDER_THEME_GUIDE,
9 HTML_RENDER_TOOL_NAME,
10} from "@t3tools/shared/htmlRender";
11import * as Schema from "effect/Schema";
12import { Tool, Toolkit } from "effect/ai";
13
14import * as HtmlRender from "../../../htmlRender/HtmlRender.ts";
15import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts";
16import * as McpInvocationContext from "../../McpInvocationContext.ts";
17
18const Html = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(512_000)).annotate({
19 description: "A complete, self-contained HTML document.",
20});
21
22const PAGE_RULES =
23 'Write one self-contained document with inline <style> and <script>. Local images written as absolute file paths (src="/abs/shot.png", CSS url(/abs/bg.webp), or a JS string) are inlined automatically; remote http(s) URLs, such as a CDN chart library, load as-is.';
24
25export const HtmlPreviewTool = Tool.make("html_preview", {
26 description: `Render an HTML page in T3's headless browser and get back a PNG screenshot, contentHeight (the height the page needs at this width), and its console output: log, info, warning, error, and uncaught exceptions, with stack traces pointing into page.html. console.log is a fine way to report your own checks. Use it to check and iterate on a page before html_render. The first preview on a machine can report that T3 is still installing its preview browser; call again a minute later. ${PAGE_RULES} The page gets the theme variables and layout described in html_render.`,
27 parameters: Schema.Struct({
28 html: Html,
29 width: Schema.optional(
30 Schema.Int.annotate({
31 description: `Viewport width in CSS pixels, 240-1600. Defaults to ${HTML_RENDER_COLUMN_WIDTH}, the reply column; use about 390 to check phones.`,
32 }),
33 ),
34 appearance: Schema.optional(
35 Schema.Literals(["dark", "light"]).annotate({
36 description: "Theme to preview. Defaults to dark.",
37 }),
38 ),
39 }),
40 success: Schema.Struct({
41 width: Schema.Int,
42 contentHeight: Schema.Int,
43 capturedHeight: Schema.Int,
44 consoleMessages: Schema.Array(
45 Schema.Struct({
46 level: Schema.Literals(["log", "info", "warning", "error"]),
47 text: Schema.String,
48 }),
49 ),
50 missingImages: Schema.optional(Schema.Array(Schema.String)),
51 screenshot: Schema.Struct({
52 mimeType: Schema.Literal("image/png"),
53 data: Schema.String,
54 width: Schema.Int,
55 height: Schema.Int,
56 }),
57 }),
58 failure: OrchestratorMcpFailure,
59 dependencies: [
60 McpInvocationContext.McpInvocationContext,
61 ThreadManagementService.ThreadManagementService,
62 HtmlRender.HtmlRender,
63 ],
64})
65 .annotate(Tool.Title, "Preview HTML")
66 .annotate(Tool.Readonly, true)
67 .annotate(Tool.Destructive, false)
68 .annotate(Tool.Idempotent, true)
69 .annotate(Tool.OpenWorld, true);
70
71// Read-only in the MCP sense: it shows a page in the caller's own thread and
72// touches no workspace, so plan mode and read-only sandboxes can use it.
73// Open-world, since the page may load remote resources, as in a preview.
74const HtmlRenderTool = Tool.make(HTML_RENDER_TOOL_NAME, {
75 description: `Show a finished HTML page (chart, table, diagram, collage, mockup) inline in this thread, above your final text reply; call it before writing that reply. The reader already sees the page, so the reply should not announce it, say where it is, or restate it: add only what the page doesn't say. Preview with html_preview first. T3 fits the frame to the page's height at each reader's width. A height below the page's contentHeight caps the frame there, and the rest scrolls inside it. ${PAGE_RULES} ${HTML_RENDER_LAYOUT_GUIDE} ${HTML_RENDER_THEME_GUIDE}`,
76 parameters: Schema.Struct({
77 html: Html,
78 title: Schema.String.check(
79 Schema.isMinLength(1),
80 Schema.isMaxLength(HTML_RENDER_MAX_TITLE_LENGTH),
81 ).annotate({ description: "Short name for the page." }),
82 height: Schema.Int.annotate({
83 description: `The frame height in CSS pixels, ${HTML_RENDER_MIN_HEIGHT}-${HTML_RENDER_MAX_HEIGHT}. Use html_preview's contentHeight, or less to make long content scroll inside the frame.`,
84 }),
85 }),
86 success: Schema.Struct({
87 htmlRender: Schema.Struct({
88 attachmentId: Schema.String,
89 title: Schema.String,
90 height: Schema.Number,
91 heights: Schema.optional(Schema.Array(Schema.Tuple([Schema.Int, Schema.Int]))),
92 }),
93 message: Schema.String,
94 }),
95 failure: OrchestratorMcpFailure,
96 failureMode: "return",
97 dependencies: [
98 McpInvocationContext.McpInvocationContext,
99 ThreadManagementService.ThreadManagementService,
100 HtmlRender.HtmlRender,
101 ],
102})
103 .annotate(Tool.Title, "Render HTML")
104 .annotate(Tool.Readonly, true)
105 .annotate(Tool.Destructive, false)
106 .annotate(Tool.Idempotent, false)
107 .annotate(Tool.OpenWorld, true);
108
109export const HtmlPreviewToolkit = Toolkit.make(HtmlPreviewTool);
110
111export const HtmlRenderToolkit = Toolkit.make(HtmlRenderTool);
112
113export const HtmlToolkit = Toolkit.make(HtmlPreviewTool, HtmlRenderTool);