Skip to content

Commit 5e4fcad

Browse files
refactor(preview): the server always owns the browser engine
Desktop-managed servers left browser tabs to Electron, behind T3CODE_SERVER_BROWSER, so agents, remote clients, and fixes split across two engines. The server browser now always runs and every environment offers server tabs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 338d0b9 commit 5e4fcad

8 files changed

Lines changed: 47 additions & 80 deletions

File tree

‎apps/server/src/environment/ServerEnvironment.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { readAgentActivityPublishingActive } from "../cloud/config.ts";
2020
import { resolveServerSelfUpdateCapability } from "../cloud/selfUpdate.ts";
2121
import { resolveServiceLauncherMode } from "../cloud/serviceLauncherClient.ts";
2222
import * as ServerConfig from "../config.ts";
23-
import { isServerBrowserEnabled } from "../preview/serverBrowserEnabled.ts";
2423
import * as ProcessRunner from "../processRunner.ts";
2524
import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts";
2625
import { detectServerEnvironmentMachineKind } from "./ServerEnvironmentMachine.ts";
@@ -258,7 +257,7 @@ export const make = Effect.gen(function* () {
258257
? { serverSelfUpdateProgress: true }
259258
: {}),
260259
...(desktopAppUpdate ? { desktopAppUpdate: true } : {}),
261-
...(isServerBrowserEnabled(serverConfig.mode) ? { serverBrowser: true } : {}),
260+
serverBrowser: true,
262261
},
263262
};
264263

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ import * as PubSub from "effect/PubSub";
3939
import * as Scope from "effect/Scope";
4040
import * as Stream from "effect/Stream";
4141
import * as SynchronizedRef from "effect/SynchronizedRef";
42-
import * as ServerConfig from "../config.ts";
4342
import { PreviewControlRequiredError } from "@t3tools/contracts";
44-
import { isServerBrowserEnabled } from "./serverBrowserEnabled.ts";
4543

4644
export class PreviewManager extends Context.Service<
4745
PreviewManager,
@@ -132,8 +130,6 @@ const currentIsoTimestamp = DateTime.now.pipe(Effect.map(DateTime.formatIso));
132130

133131
/** @public Service construction is part of the canonical Effect module API. */
134132
export const make = Effect.gen(function* PreviewManagerMake() {
135-
const config = yield* ServerConfig.ServerConfig;
136-
const serverBrowser = isServerBrowserEnabled(config.mode);
137133
const serverEpoch = NodeCrypto.randomUUID();
138134
const stateRef = yield* SynchronizedRef.make<ManagerState>(initialState);
139135
// Unbounded PubSub is fine here — events are tiny and we don't want to
@@ -200,8 +196,7 @@ export const make = Effect.gen(function* PreviewManagerMake() {
200196

201197
const open: PreviewManager["Service"]["open"] = Effect.fn("PreviewManager.open")(
202198
function* (input) {
203-
// Without a server browser nothing renders server tabs, so the client renders it.
204-
const runtime = input.runtime === "server" && !serverBrowser ? undefined : input.runtime;
199+
const runtime = input.runtime;
205200
// Persisted client surfaces must not bind to a different tab after a server restart.
206201
const tabId = `${newPreviewTabId()}${runtime === "server" ? `_${serverEpoch}` : ""}`;
207202
const updatedAt = yield* currentIsoTimestamp;

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import * as Layer from "effect/Layer";
1818
import * as Option from "effect/Option";
1919
import * as Queue from "effect/Queue";
2020
import type { BrowserContext, Page } from "playwright-core";
21-
import { afterEach, beforeEach, expect, vi } from "vite-plus/test";
21+
import { beforeEach, expect, vi } from "vite-plus/test";
2222

2323
import * as ServerConfig from "../config.ts";
2424
import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
@@ -188,9 +188,7 @@ beforeEach(() => {
188188
contexts.length = 0;
189189
contextGate = null;
190190
contextFailure = null;
191-
vi.stubEnv("T3CODE_SERVER_BROWSER", "1");
192191
});
193-
afterEach(() => vi.unstubAllEnvs());
194192

195193
it.live("readiness none responds immediately but takeover input waits for navigation commit", () =>
196194
Effect.scoped(

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

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ import * as ServerBrowserPage from "./ServerBrowserPage.ts";
6767
import * as ServerBrowserToolchain from "./ServerBrowserToolchain.ts";
6868
import { ServerBrowserContexts } from "./ServerBrowserContexts.ts";
6969
import { BrowserControlInterrupted, SessionControl } from "./SessionControl.ts";
70-
import { isServerBrowserEnabled } from "./serverBrowserEnabled.ts";
7170

7271
const SERVER_HOST_CLIENT_ID = "server-browser";
7372
const RENDER_SCALE = 2;
@@ -186,7 +185,6 @@ export interface ServerBrowserViewer {
186185
export class ServerBrowser extends Context.Service<
187186
ServerBrowser,
188187
{
189-
readonly enabled: boolean;
190188
readonly attachViewer: (input: {
191189
readonly threadId: string;
192190
readonly tabId: string;
@@ -407,7 +405,6 @@ const CLIPBOARD_SCRIPT = `(() => {
407405

408406
const make = Effect.gen(function* () {
409407
const config = yield* ServerConfig.ServerConfig;
410-
const enabled = isServerBrowserEnabled(config.mode);
411408
const manager = yield* PreviewManager.PreviewManager;
412409
const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;
413410
const environment = yield* ServerEnvironment.ServerEnvironment;
@@ -1935,51 +1932,49 @@ const make = Effect.gen(function* () {
19351932
} satisfies ServerBrowserViewer;
19361933
});
19371934

1938-
if (enabled) {
1939-
yield* manager.events.pipe(Stream.runForEach(mirrorManagerEvent), Effect.forkScoped);
1940-
yield* Effect.sync(closeIdleAgentTabs).pipe(
1941-
Effect.repeat(Schedule.spaced(IDLE_SWEEP_INTERVAL)),
1942-
Effect.forkScoped,
1943-
);
1944-
const environmentId = yield* environment.getEnvironmentId;
1945-
const hostSession = broker
1946-
.connect(
1947-
{
1948-
clientId: SERVER_HOST_CLIENT_ID,
1949-
environmentId,
1950-
supportedOperations: [...PREVIEW_AUTOMATION_SERVER_OPERATIONS],
1951-
},
1952-
{ preferred: true },
1953-
)
1954-
.pipe(
1955-
Effect.flatMap((events) =>
1956-
events.pipe(
1957-
Stream.runForEach((event) => {
1958-
if (event.type === "connected") {
1959-
hostConnectionId = event.connectionId;
1960-
return Effect.sync(reportLiveTabs);
1961-
}
1962-
return handleRequest(event.connectionId, event.request).pipe(
1963-
Effect.forkScoped,
1964-
Effect.asVoid,
1965-
);
1966-
}),
1967-
),
1935+
yield* manager.events.pipe(Stream.runForEach(mirrorManagerEvent), Effect.forkScoped);
1936+
yield* Effect.sync(closeIdleAgentTabs).pipe(
1937+
Effect.repeat(Schedule.spaced(IDLE_SWEEP_INTERVAL)),
1938+
Effect.forkScoped,
1939+
);
1940+
const environmentId = yield* environment.getEnvironmentId;
1941+
const hostSession = broker
1942+
.connect(
1943+
{
1944+
clientId: SERVER_HOST_CLIENT_ID,
1945+
environmentId,
1946+
supportedOperations: [...PREVIEW_AUTOMATION_SERVER_OPERATIONS],
1947+
},
1948+
{ preferred: true },
1949+
)
1950+
.pipe(
1951+
Effect.flatMap((events) =>
1952+
events.pipe(
1953+
Stream.runForEach((event) => {
1954+
if (event.type === "connected") {
1955+
hostConnectionId = event.connectionId;
1956+
return Effect.sync(reportLiveTabs);
1957+
}
1958+
return handleRequest(event.connectionId, event.request).pipe(
1959+
Effect.forkScoped,
1960+
Effect.asVoid,
1961+
);
1962+
}),
19681963
),
1969-
);
1970-
// The broker disconnects timed-out hosts, including slow first installs. Reconnect.
1971-
yield* hostSession.pipe(
1972-
Effect.exit,
1973-
Effect.andThen(Effect.sleep(HOST_RECONNECT_DELAY)),
1974-
Effect.forever,
1975-
Effect.forkScoped,
1976-
);
1977-
yield* Effect.addFinalizer(() =>
1978-
Effect.promise(async () => {
1979-
await contexts.close();
1980-
}),
1964+
),
19811965
);
1982-
}
1966+
// The broker disconnects timed-out hosts, including slow first installs. Reconnect.
1967+
yield* hostSession.pipe(
1968+
Effect.exit,
1969+
Effect.andThen(Effect.sleep(HOST_RECONNECT_DELAY)),
1970+
Effect.forever,
1971+
Effect.forkScoped,
1972+
);
1973+
yield* Effect.addFinalizer(() =>
1974+
Effect.promise(async () => {
1975+
await contexts.close();
1976+
}),
1977+
);
19831978

19841979
const clearProfile = (profileId: string) =>
19851980
Effect.tryPromise({
@@ -1988,7 +1983,6 @@ const make = Effect.gen(function* () {
19881983
});
19891984

19901985
return ServerBrowser.of({
1991-
enabled,
19921986
attachViewer,
19931987
clearProfile,
19941988
openDownload,

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ it.effect.each([
8282
ack: Effect.sync(() => acked.resolve()),
8383
});
8484
const browser = ServerBrowser.ServerBrowser.of({
85-
enabled: true,
8685
clearProfile: () => Effect.void,
8786
openDownload: () => Effect.succeedNone,
8887
answerFileChooser: () => Effect.succeed(false),
@@ -159,7 +158,6 @@ it.effect.each([
159158
const auth = makeAuth(testCase.scopes, testCase.error);
160159
let attachments = 0;
161160
const browser = ServerBrowser.ServerBrowser.of({
162-
enabled: true,
163161
clearProfile: () => Effect.void,
164162
openDownload: () => Effect.succeedNone,
165163
answerFileChooser: () => Effect.succeed(false),
@@ -200,7 +198,6 @@ it.effect("serves a tab's download only to an authorized session", () =>
200198
yield* fs.writeFileString(path, "report contents");
201199
const requests: Array<unknown> = [];
202200
const browser = ServerBrowser.ServerBrowser.of({
203-
enabled: true,
204201
clearProfile: () => Effect.void,
205202
openDownload: (input) =>
206203
Effect.sync(() => {
@@ -248,7 +245,6 @@ it.effect("passes uploaded files to the page's open picker and needs operate sco
248245
Effect.gen(function* () {
249246
const answers: Array<{ chooserId: string; files: Array<{ name: string; text: string }> }> = [];
250247
const browser = ServerBrowser.ServerBrowser.of({
251-
enabled: true,
252248
clearProfile: () => Effect.void,
253249
openDownload: () => Effect.succeedNone,
254250
answerFileChooser: (input) =>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const makeHandler = (browser: ServerBrowser.ServerBrowser["Service"]) =>
6969
params.get("interactive") !== "false";
7070
const threadId = params.get("threadId") ?? "";
7171
const tabId = params.get("tabId") ?? "";
72-
if (!browser.enabled || threadId.length === 0 || tabId.length === 0) {
72+
if (threadId.length === 0 || tabId.length === 0) {
7373
return HttpServerResponse.text("Not Found", { status: 404 });
7474
}
7575
return yield* Effect.scoped(

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

Lines changed: 0 additions & 13 deletions
This file was deleted.

‎docs/user/remote-access.md‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,11 @@ For Antigravity's Google callback on a remote host, see
177177
On an environment started with `t3 serve` or `npx t3`, browser tabs run on the
178178
host instead of your device. You can watch and use them from a phone or any
179179
other browser. Agents keep using them while no device is connected, and
180-
`localhost` addresses reach servers on the host. The desktop app's own
181-
environment keeps using the desktop browser.
180+
`localhost` addresses reach servers on the host.
182181

183182
The first tab downloads a headless Chromium and fonts, about 120 MB, into the
184183
T3 home. To use a browser already installed on the host, set
185-
`T3CODE_PREVIEW_BROWSER_PATH` to its executable. Set `T3CODE_SERVER_BROWSER=0`
186-
to turn host browser tabs off.
184+
`T3CODE_PREVIEW_BROWSER_PATH` to its executable.
187185

188186
Agent tabs have separate storage and share a Chromium process. Take control before
189187
typing into an agent's tab, then release control when you want the agent to

0 commit comments

Comments
 (0)