Skip to content

Commit b1b659a

Browse files
refactor(preview): server browser tabs and HTML renders share one headless browser
A host that used both downloaded two Chromiums: the server browser's Sparticuz pack plus fonts, and the HTML renderer's pinned Chrome for Testing headless shell. PreviewBrowser now lives in preview/ and is provided once in server.ts, so ServerBrowser and the HTML toolkit run the same install. Its messages no longer name html_preview, since agents reach it from either feature. ServerBrowserToolchain and its system-browser discovery are gone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 8a97006 commit b1b659a

15 files changed

Lines changed: 39 additions & 586 deletions

‎apps/server/src/htmlRender/HtmlRender.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import * as NodeURL from "node:url";
2424
import { resolveAttachmentPathById } from "../attachmentStore.ts";
2525
import * as ServerConfig from "../config.ts";
2626
import * as HtmlRender from "./HtmlRender.ts";
27-
import * as PreviewBrowser from "./PreviewBrowser.ts";
27+
import * as PreviewBrowser from "../preview/PreviewBrowser.ts";
2828

2929
// Real-browser tests run only when this names a chrome-headless-shell, for
3030
// example one T3 installed under <T3 home>/tools/chrome-headless-shell.

‎apps/server/src/htmlRender/HtmlRender.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { resolveAttachmentRelativePath } from "../attachmentPaths.ts";
3131
import { createAttachmentId } from "../attachmentStore.ts";
3232
import * as ServerConfig from "../config.ts";
3333
import * as HeadlessChrome from "./headlessChrome.ts";
34-
import * as PreviewBrowser from "./PreviewBrowser.ts";
34+
import * as PreviewBrowser from "../preview/PreviewBrowser.ts";
3535

3636
const MIB = 1024 * 1024;
3737
const MAX_IMAGE_BYTES = 10 * MIB;

‎apps/server/src/mcp/McpHttpServer.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import packageJson from "../../package.json" with { type: "json" };
1919
import * as ServerConfig from "../config.ts";
2020
import * as DeviceService from "../device/DeviceService.ts";
2121
import * as HtmlRender from "../htmlRender/HtmlRender.ts";
22-
import * as PreviewBrowser from "../htmlRender/PreviewBrowser.ts";
2322
import * as McpInvocationContext from "./McpInvocationContext.ts";
2423
import * as OrchestratorMcpService from "./OrchestratorMcpService.ts";
2524
import { PreviewControlsToolkit } from "./toolkits/previewControls/tools.ts";
@@ -700,7 +699,7 @@ const registerHtmlPreview = Effect.fn("McpHttpServer.registerHtmlPreview")(funct
700699
export const HtmlToolkitRegistrationLive = Layer.mergeAll(
701700
McpServer.toolkit(HtmlRenderToolkit).pipe(Layer.provide(HtmlRenderToolkitHandlersLive)),
702701
Layer.effectDiscard(registerHtmlPreview()).pipe(Layer.provide(HtmlPreviewToolkitHandlersLive)),
703-
).pipe(Layer.provide(HtmlRender.layer), Layer.provide(PreviewBrowser.layer));
702+
).pipe(Layer.provide(HtmlRender.layer));
704703

705704
const PreviewStandardToolkitRegistrationLive = McpServer.toolkit(PreviewStandardToolkit).pipe(
706705
Layer.provide(PreviewStandardToolkitHandlersLive),

‎apps/server/src/mcp/toolkits/core.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import * as ServerConfig from "../../config.ts";
2121
import { OrchestratorProjectionError } from "../../orchestration-v2/Orchestrator.ts";
2222
import * as ProviderAdapterRegistry from "../../orchestration-v2/ProviderAdapterRegistry.ts";
2323
import * as ThreadManagement from "../../orchestration-v2/ThreadManagementService.ts";
24+
import * as PreviewBrowser from "../../preview/PreviewBrowser.ts";
2425
import * as ProjectService from "../../project/ProjectService.ts";
2526
import * as ProviderRegistry from "../../provider/Services/ProviderRegistry.ts";
2627
import * as ScheduledTaskService from "../../scheduledTasks/ScheduledTaskService.ts";
@@ -204,6 +205,7 @@ it.effect("returns an HTML render reference that Codex and Claude tool rows both
204205
Effect.provide(
205206
McpHttpServer.HtmlToolkitRegistrationLive.pipe(
206207
Layer.provideMerge(McpServer.McpServer.layer),
208+
Layer.provide(PreviewBrowser.layer),
207209
Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-html-render-" })),
208210
Layer.provide(NodeServices.layer),
209211
// The preview browser is not installed in a fresh home, so nothing downloads.

‎apps/server/src/mcp/toolkits/worktree/registration.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import * as VcsStatusBroadcaster from "../../../vcs/VcsStatusBroadcaster.ts";
2424
import * as McpHttpServer from "../../McpHttpServer.ts";
2525
import * as McpSessionRegistry from "../../McpSessionRegistry.ts";
2626
import * as PreviewAutomationBroker from "../../PreviewAutomationBroker.ts";
27+
import * as PreviewBrowser from "../../../preview/PreviewBrowser.ts";
2728

2829
const StubServicesLive = Layer.mergeAll(
2930
Layer.mock(Orchestrator.OrchestratorV2)({}),
@@ -75,6 +76,7 @@ it.effect("production mcp layer lists worktree tools over http", () =>
7576
}),
7677
),
7778
Layer.provide(PreviewAutomationBroker.layer),
79+
Layer.provide(PreviewBrowser.layer),
7880
Layer.provide(StubServicesLive),
7981
Layer.build,
8082
);

apps/server/src/htmlRender/PreviewBrowser.test.ts renamed to apps/server/src/preview/PreviewBrowser.test.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,7 @@ it.layer(NodeServices.layer)("PreviewBrowser", (it) => {
171171
const error = yield* browser.executable.pipe(Effect.flip);
172172

173173
expect(error._tag).toBe("PreviewBrowserInstallError");
174-
expect(error.message).toMatch(
175-
/^T3 could not install its HTML preview browser: .+ Call html_preview again to retry\.$/,
176-
);
174+
expect(error.message).toMatch(/^T3 could not install its headless browser: .+ Try again\.$/);
177175
expect(yield* fs.readDirectory(installRoot)).toEqual([]);
178176
yield* browser.executable.pipe(Effect.flip);
179177
expect(requests).toHaveLength(2);
@@ -290,7 +288,5 @@ it("tells the agent how far the install has come", () => {
290288
totalBytes: 120_477_194,
291289
unpacking: false,
292290
}).message,
293-
).toBe(
294-
"T3 is installing its HTML preview browser (37 of 120 MB downloaded). Call html_preview again in a minute.",
295-
);
291+
).toBe("T3 is installing its headless browser (37 of 120 MB downloaded). Try again in a minute.");
296292
});

apps/server/src/htmlRender/PreviewBrowser.ts renamed to apps/server/src/preview/PreviewBrowser.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import * as NodeCrypto from "node:crypto";
2121
import * as ServerConfig from "../config.ts";
2222
import { openZipArchive } from "../zipArchive.ts";
2323

24-
// HTML previews only ever run T3's own pinned Chrome for Testing headless
25-
// shell, never a browser the user installed. To bump the pin, pick a version
24+
// The one browser T3 installs on a host. HTML render previews and server
25+
// browser tabs both run this pinned Chrome for Testing headless shell, so a host
26+
// downloads it once; neither uses a browser the user installed. To bump the pin, pick a version
2627
// from https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json,
2728
// download each platform's chrome-headless-shell zip, and replace the version
2829
// and every byte count and SHA-256 below. Hosts drop the old build after the
@@ -100,7 +101,7 @@ export class PreviewBrowserInstallError extends Schema.TaggedError<PreviewBrowse
100101
{ detail: Schema.String, cause: Schema.optional(Schema.Defect()) },
101102
) {
102103
override get message(): string {
103-
return `T3 could not install its HTML preview browser: ${this.detail} Call html_preview again to retry.`;
104+
return `T3 could not install its headless browser: ${this.detail} Try again.`;
104105
}
105106
}
106107
const isInstallError = Schema.is(PreviewBrowserInstallError);
@@ -113,7 +114,7 @@ export class PreviewBrowserInstallingError extends Schema.TaggedError<PreviewBro
113114
const progress = this.unpacking
114115
? "unpacking"
115116
: `${megabytes(this.downloadedBytes)} of ${megabytes(this.totalBytes)} MB downloaded`;
116-
return `T3 is installing its HTML preview browser (${progress}). Call html_preview again in a minute.`;
117+
return `T3 is installing its headless browser (${progress}). Try again in a minute.`;
117118
}
118119
}
119120

@@ -122,7 +123,7 @@ export class PreviewBrowserUnsupportedError extends Schema.TaggedError<PreviewBr
122123
{ platform: Schema.String, arch: Schema.String },
123124
) {
124125
override get message(): string {
125-
return `HTML previews are not available on ${this.platform}-${this.arch}: Chrome for Testing has no headless shell for it. html_render still works without a preview.`;
126+
return `T3's headless browser is not available on ${this.platform}-${this.arch}: Chrome for Testing has no headless shell for it.`;
126127
}
127128
}
128129

@@ -141,7 +142,7 @@ export class PreviewBrowser extends Context.Service<
141142
/** The installed headless shell, if any. Never starts or waits on an install. */
142143
readonly installed: Effect.Effect<Option.Option<string>>;
143144
}
144-
>()("t3/htmlRender/PreviewBrowser") {}
145+
>()("t3/preview/PreviewBrowser") {}
145146

146147
export interface PreviewBrowserOptions {
147148
readonly baseDir: string;

‎apps/server/src/preview/ServerBrowser.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import * as Broker from "../mcp/PreviewAutomationBroker.ts";
2727
import * as DesktopChannel from "./DesktopBrowserChannel.ts";
2828
import * as Manager from "./Manager.ts";
2929
import * as ServerBrowser from "./ServerBrowser.ts";
30-
import * as Toolchain from "./ServerBrowserToolchain.ts";
30+
import * as PreviewBrowser from "./PreviewBrowser.ts";
3131

3232
// Keep the manager, broker, ownership, refs, and viewer paths real; replace Chromium I/O only.
3333
vi.mock("./ServerBrowserContexts.ts", () => ({
@@ -189,8 +189,9 @@ const dependencies = Layer.mergeAll(
189189
getEnvironmentId: Effect.succeed(scope.environmentId),
190190
getDescriptor: Effect.die("unused descriptor"),
191191
}),
192-
Layer.succeed(Toolchain.ServerBrowserToolchain, {
193-
resolve: Effect.die("mock Chromium does not need an executable"),
192+
Layer.succeed(PreviewBrowser.PreviewBrowser, {
193+
executable: Effect.die("mock Chromium does not need an executable"),
194+
installed: Effect.die("mock Chromium does not need an executable"),
194195
}),
195196
Layer.succeed(DesktopChannel.DesktopBrowserChannel, {
196197
// Only tabs a test marks render on the desktop; the rest stay headless.

‎apps/server/src/preview/ServerBrowser.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ import * as PreviewAutomationBroker from "../mcp/PreviewAutomationBroker.ts";
6666
import * as DesktopBrowserChannel from "./DesktopBrowserChannel.ts";
6767
import * as PreviewManager from "./Manager.ts";
6868
import * as ServerBrowserPage from "./ServerBrowserPage.ts";
69-
import * as ServerBrowserToolchain from "./ServerBrowserToolchain.ts";
69+
import * as PreviewBrowser from "./PreviewBrowser.ts";
7070
import { ServerBrowserContexts } from "./ServerBrowserContexts.ts";
7171
import { BrowserControlInterrupted, SessionControl } from "./SessionControl.ts";
7272

@@ -419,7 +419,7 @@ const make = Effect.gen(function* () {
419419
const manager = yield* PreviewManager.PreviewManager;
420420
const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;
421421
const environment = yield* ServerEnvironment.ServerEnvironment;
422-
const toolchain = yield* ServerBrowserToolchain.ServerBrowserToolchain;
422+
const previewBrowser = yield* PreviewBrowser.PreviewBrowser;
423423
const desktopChannel = yield* DesktopBrowserChannel.DesktopBrowserChannel;
424424
const runFork = Effect.runForkWith(yield* Effect.context<never>());
425425

@@ -436,7 +436,7 @@ const make = Effect.gen(function* () {
436436

437437
const contexts = new ServerBrowserContexts({
438438
profilesDir: NodePath.join(config.stateDir, "server-browser", "profiles"),
439-
resolve: () => Effect.runPromise(toolchain.resolve),
439+
executable: () => Effect.runPromise(previewBrowser.executable),
440440
onContextClose: (context) => {
441441
for (const tab of tabs.values()) {
442442
if (tab.page.context() === context) dropTab(tab, true);

‎apps/server/src/preview/ServerBrowserContexts.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const makeBrowser = () => {
5252

5353
const options = () => ({
5454
profilesDir: "/test/profiles",
55-
resolve: vi.fn(async () => ({ executablePath: "/test/chromium", env: { LIB_PATH: "browser" } })),
55+
executable: vi.fn(async () => "/test/chromium"),
5656
env: {},
5757
});
5858

@@ -70,7 +70,7 @@ describe("ServerBrowserContexts", () => {
7070
launches.launch.mockReturnValue(launch.promise);
7171
const configuration = options();
7272
const pool = new ServerBrowserContexts(configuration);
73-
expect(configuration.resolve).not.toHaveBeenCalled();
73+
expect(configuration.executable).not.toHaveBeenCalled();
7474
const first = pool.contextFor("default", "agent-a");
7575
const again = pool.contextFor("default", "agent-a");
7676
const second = pool.contextFor("default", "agent-b");

0 commit comments

Comments
 (0)