Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fd394ac
fix(acp): Antigravity html_render pages show in the thread
neo-clon Oct 15, 2026
021c734
fix(acp): a repeated html_render completion keeps its page
neo-clon Oct 15, 2026
d55f3e5
fix(acp): an html_render that returned an error keeps the error
neo-clon Oct 15, 2026
d935233
fix(acp): an html_render that reports its page takes the kept page
neo-clon Oct 15, 2026
59e737b
fix(acp): an html_render call takes the page published with its html
neo-clon Oct 15, 2026
465c13b
fix(acp): each thread keeps its own most recent html_render pages
neo-clon Oct 15, 2026
34b2c12
Merge remote-tracking branch 'origin/main' into fix/antigravity-html-…
neo-clon Oct 16, 2026
2329bc5
fix(server): interrupted html_render publish rolls back file and sess…
neo-clon Oct 16, 2026
db808e1
Merge branch 'main' into fix/antigravity-html-render
neo-clon Oct 16, 2026
21e9dc6
Merge branch 'main' into fix/antigravity-html-render
neo-clon Oct 16, 2026
4cc2dfd
Merge branch 'main' into fix/antigravity-html-render
neo-clon Oct 16, 2026
f27e747
Merge branch 'main' into fix/antigravity-html-render
neo-clon Oct 16, 2026
fbba118
Merge branch 'main' into fix/antigravity-html-render
neo-clon Oct 17, 2026
bf0bf31
Merge branch 'main' into fix/antigravity-html-render
neo-clon Oct 17, 2026
b6ea4ff
Merge branch 'main' into fix/antigravity-html-render
neo-clon Oct 17, 2026
1db4811
Merge origin/main into fix/antigravity-html-render
juliusmarminge Oct 19, 2026
96ee923
fix(acp): retain Antigravity published pages with focused coverage
juliusmarminge Oct 19, 2026
9915442
Merge origin/main into fix/antigravity-html-render
juliusmarminge Oct 19, 2026
a89315c
Merge remote-tracking branch 'origin/main' into fix/antigravity-html-…
juliusmarminge Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 55 additions & 11 deletions apps/server/src/htmlRender/HtmlRender.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// @effect-diagnostics nodeBuiltinImport:off - plain local servers stand in for LAN services.
import * as NodeServices from "@effect/platform-node/NodeServices";
import { describe, expect, it } from "@effect/vitest";
import { ThreadId } from "@t3tools/contracts";
import { EnvironmentId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
import * as HostProcess from "@t3tools/shared/HostProcess";
import {
HTML_RENDER_MEASURE_FONTS,
Expand Down Expand Up @@ -47,6 +48,7 @@ const layerHtmlRender = (
}),
),
),
Layer.provideMerge(McpProviderSessions.layer),
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-html-render-" })),
Layer.provideMerge(NodeServices.layer),
);
Expand Down Expand Up @@ -196,10 +198,22 @@ describe("HtmlRender", () => {
const fileSystem = yield* FileSystem.FileSystem;
const config = yield* ServerConfig.ServerConfig;
const htmlRender = yield* HtmlRender.HtmlRender;
const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
const threadId = ThreadId.make("thread-html-render");
const session = {
environmentId: EnvironmentId.make("html-render-environment"),
threadId,
providerSessionId: "first-credential",
providerInstanceId: ProviderInstanceId.make("claude"),
endpoint: "http://localhost:8311/mcp",
authorizationHeader: "Bearer first-token",
browserToolsAvailable: false,
};
yield* mcpSessions.set(session);

const reference = yield* htmlRender.publish({
threadId: ThreadId.make("thread-html-render"),
html: "<p>Quarterly revenue</p>",
threadId,
html: `<p>Quarterly revenue</p>${" ".repeat(8_000)}`,
title: " Revenue ",
height: 9_000,
});
Expand All @@ -217,32 +231,62 @@ describe("HtmlRender", () => {
const html = yield* fileSystem.readFileString(stored ?? "");
expect(html).toContain('<style id="t3-theme">');
expect(html).toContain("<p>Quarterly revenue</p>");
// Antigravity reports the call without its result, so the thread's
// provider session keeps the page for one call with these arguments.
const page = {
html: `<p>Quarterly revenue</p>${" ".repeat(8_000)}`,
title: " Revenue ",
height: 9_000,
};
expect(
yield* mcpSessions.takeHtmlRender(threadId, {
...page,
html: page.html.replace("revenue", "expense"),
}),
).toBe(undefined);
expect(yield* mcpSessions.takeHtmlRender(threadId, page)).toEqual(reference);
expect(yield* mcpSessions.takeHtmlRender(threadId, page)).toBe(undefined);
const reused = yield* htmlRender.publish({ threadId, ...page });
yield* mcpSessions.set({ ...session, browserToolsAvailable: true });
expect(yield* mcpSessions.takeHtmlRender(threadId, page)).toEqual(reused);

yield* htmlRender.publish({ threadId, ...page });
yield* mcpSessions.set({
...session,
providerSessionId: "replacement-credential",
providerInstanceId: ProviderInstanceId.make("antigravity"),
authorizationHeader: "Bearer replacement-token",
});
const replacement = yield* htmlRender.publish({ threadId, ...page });
expect(yield* mcpSessions.takeHtmlRender(threadId, page)).toEqual(replacement);
expect(yield* mcpSessions.takeHtmlRender(threadId, page)).toBe(undefined);
// Clearing the thread's MCP session drops the pages it kept.
yield* htmlRender.publish({ threadId, ...page });
yield* mcpSessions.clear(threadId);
expect(yield* mcpSessions.takeHtmlRender(threadId, page)).toBe(undefined);
}).pipe(Effect.provide(layerTest)),
);

it.effect("removes the page when publishing is interrupted", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("thread-html-cancel");
const page = { html: "<p>x</p>", title: "X", height: 200 };
const measuring = yield* Deferred.make<void>();
yield* Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const config = yield* ServerConfig.ServerConfig;
const htmlRender = yield* HtmlRender.HtmlRender;
const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
const storedPages = fileSystem
.readDirectory(config.attachmentsDir, { recursive: true })
.pipe(Effect.map((names) => names.filter((name) => name.endsWith(".html"))));

const publishing = yield* htmlRender
.publish({
threadId: ThreadId.make("thread-html-cancel"),
html: "<p>x</p>",
title: "X",
height: 200,
})
.pipe(Effect.forkChild);
const publishing = yield* htmlRender.publish({ threadId, ...page }).pipe(Effect.forkChild);
yield* Deferred.await(measuring);
expect(yield* storedPages).toHaveLength(1);
yield* Fiber.interrupt(publishing);
expect(yield* storedPages).toEqual([]);
expect(yield* mcpSessions.takeHtmlRender(threadId, page)).toBe(undefined);
}).pipe(
Effect.provide(
layerHtmlRender(
Expand Down
38 changes: 28 additions & 10 deletions apps/server/src/htmlRender/HtmlRender.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ThreadId } from "@t3tools/contracts";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
import * as HostProcess from "@t3tools/shared/HostProcess";
import {
clampHtmlRenderHeight,
Expand Down Expand Up @@ -310,6 +311,7 @@ const make = Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const config = yield* ServerConfig.ServerConfig;
const previewBrowser = yield* PreviewBrowser.PreviewBrowser;
const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
const services = yield* Effect.context<
FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
>();
Expand Down Expand Up @@ -405,19 +407,35 @@ const make = Effect.gen(function* () {
if (attachmentId === null || filePath === null) {
return yield* new HtmlRenderStoreError({ cause: new Error("Invalid thread id.") });
}
const heights = yield* fileSystem.writeFileString(filePath, html).pipe(
Effect.mapError((cause) => new HtmlRenderStoreError({ cause })),
Effect.andThen(measure(html)),
// Only the returned reference lets thread deletion find the page, so a
// publish that fails or is interrupted before returning removes it.
Effect.onError(() => fileSystem.remove(filePath, { force: true }).pipe(Effect.ignore)),
);
return {
const baseReference: HtmlRenderReference = {
attachmentId,
title: input.title.trim().slice(0, HTML_RENDER_MAX_TITLE_LENGTH) || "HTML",
height: clampHtmlRenderHeight(input.height),
...(heights === undefined ? {} : { heights }),
} satisfies HtmlRenderReference;
};
return yield* fileSystem.writeFileString(filePath, html).pipe(
Effect.mapError((cause) => new HtmlRenderStoreError({ cause })),
Effect.uninterruptible,
Effect.andThen(measure(html)),
Effect.andThen((heights) => {
const reference: HtmlRenderReference = {
...baseReference,
...(heights === undefined ? {} : { heights }),
};
return mcpSessions
.recordHtmlRender(input.threadId, input, reference)
.pipe(Effect.as(reference));
}),
// An interrupted publish must leave neither an attachment nor a recoverable reference.
Effect.onError(() =>
Effect.all(
[
fileSystem.remove(filePath, { force: true }).pipe(Effect.ignore),
mcpSessions.takeHtmlRender(input.threadId, input, baseReference).pipe(Effect.ignore),
],
{ discard: true },
),
),
);
});

const preview = Effect.fn("HtmlRender.preview")(function* (input: {
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/mcp/toolkits/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type OrchestrationV2ProjectedTurnItem,
type OrchestrationV2ThreadShell,
} from "@t3tools/contracts";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as TestClock from "effect/testing/TestClock";
Expand Down Expand Up @@ -292,6 +293,7 @@ it.effect("returns an HTML render reference that Codex and Claude tool rows both
Effect.provide(
McpHttpServer.layerHtmlToolkit.pipe(
Layer.provideMerge(McpServer.McpServer.layer),
Layer.provide(McpProviderSessions.layer),
Layer.provide(PreviewBrowser.layer),
Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-html-render-" })),
Layer.provide(NodeServices.layer),
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/mcp/toolkits/worktree/registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { expect, it } from "@effect/vitest";
import { NodeHttpServer } from "@effect/platform-node";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { ProviderInstanceId, ThreadId } from "@t3tools/contracts";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
Expand Down Expand Up @@ -57,6 +58,7 @@ const layerStubServices = Layer.mergeAll(
Layer.mock(SourceControlRepositoryService.SourceControlRepositoryService)({}),
Layer.mock(ThreadLaunchService.ThreadLaunchService)({}),
Layer.mock(ThreadSearch.ThreadSearch)({}),
McpProviderSessions.layer,
);

const ToolsListPayload = Schema.fromJsonString(
Expand Down
Loading
Loading