Skip to content
Closed
1 change: 1 addition & 0 deletions apps/server/src/preview/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export const make = Effect.gen(function* PreviewManagerMake() {
canGoForward: false,
viewport: input.viewport ?? FILL_PREVIEW_VIEWPORT,
...(input.profileId === undefined ? {} : { profileId: input.profileId }),
...(input.colorScheme === undefined ? {} : { colorScheme: input.colorScheme }),
...(runtime === undefined ? {} : { runtime }),
...(runtime === "server" && input.automationOwner !== undefined
? { automationOwner: input.automationOwner }
Expand Down
52 changes: 52 additions & 0 deletions apps/server/src/preview/ServerBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,58 @@ it.live("drives the desktop's own page for a tab the desktop renders", () =>
).pipe(Effect.provide(layer)),
);

it.live("a system tab clears the light Playwright forces on attach, without redoing it", () =>
Effect.scoped(
Effect.gen(function* () {
const browser = yield* ServerBrowser.ServerBrowser;
const manager = yield* Manager.PreviewManager;
yield* Effect.yieldNow;
desktopRendersNext = true;
const opened = yield* manager.open({ threadId: scope.thread.threadId, runtime: "server" });
yield* browser.attachViewer(viewerInput(opened.tabId, true));
const page = desktopConnections[0]!.context.page;
expect(page.emulateMedia.mock.calls).toEqual([[{ colorScheme: null }]]);
const dark = Promise.withResolvers<void>();
page.emulateMedia.mockImplementation(async () => dark.resolve());
const target = { threadId: scope.thread.threadId, tabId: opened.tabId };
yield* manager.adjust({ ...target, zoomFactor: 1.25 });
yield* manager.adjust({ ...target, colorScheme: "dark" });
yield* Effect.promise(() => dark.promise);
expect(page.emulateMedia.mock.calls).toEqual([
[{ colorScheme: null }],
[{ colorScheme: "dark" }],
]);
}),
).pipe(Effect.provide(layer)),
);

it.live("a tab opens at its requested appearance and its popups keep it", () =>
Effect.scoped(
Effect.gen(function* () {
const browser = yield* ServerBrowser.ServerBrowser;
const manager = yield* Manager.PreviewManager;
yield* Effect.yieldNow;
const opened = yield* manager.open({
threadId: scope.thread.threadId,
runtime: "server",
colorScheme: "dark",
});
yield* browser.attachViewer(viewerInput(opened.tabId, true));
expect(contexts[0]!.page.emulateMedia).toHaveBeenCalledWith({ colorScheme: "dark" });
const popup = makeContext();
const applied = Promise.withResolvers<void>();
popup.page.emulateMedia.mockImplementation(async () => applied.resolve());
contexts[0]!.page.emit("popup", popup.page);
yield* Effect.promise(() => applied.promise);
expect(popup.page.emulateMedia.mock.calls).toEqual([[{ colorScheme: "dark" }]]);
const sessions = (yield* manager.list({ threadId: scope.thread.threadId })).sessions;
expect(sessions.find((session) => session.tabId !== opened.tabId)).toMatchObject({
colorScheme: "dark",
});
}),
).pipe(Effect.provide(layer)),
);

it.live("desktop file pickers stay native outside agent actions and remote control", () =>
Effect.scoped(
Effect.gen(function* () {
Expand Down
6 changes: 4 additions & 2 deletions apps/server/src/preview/ServerBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,8 @@ interface ServerTab {
} | null;
dialog: Dialog | null;
setting: PreviewViewportSetting;
colorScheme: PreviewAppearancePreference;
/** Null until first applied, so a `system` tab still clears Playwright's default light. */
colorScheme: PreviewAppearancePreference | null;
zoomFactor: number;
loading: boolean;
closing: boolean;
Expand Down Expand Up @@ -771,7 +772,7 @@ const make = Effect.gen(function* () {
fileChooser: null,
dialog: null,
setting: snapshot.viewport ?? FILL_PREVIEW_VIEWPORT,
colorScheme: "system",
colorScheme: null,
zoomFactor: 1,
loading: false,
closing: false,
Expand Down Expand Up @@ -1193,6 +1194,7 @@ const make = Effect.gen(function* () {
...(/^https?:/i.test(url) ? { url } : {}),
runtime: "server",
...(opener.profileId === undefined ? {} : { profileId: opener.profileId }),
...(opener.colorScheme === null ? {} : { colorScheme: opener.colorScheme }),
...(opener.control.agentId === null
? {}
: { automationOwner: opener.control.agentId, reveal: false }),
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/browser/openFileInPreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export async function openUrlInPreview<E>(input: {
url: input.url,
viewport: browserDefaultOpenViewport(defaults),
profileId: input.profileId ?? browserDefaultOpenProfileId(defaults),
...(runtime === undefined ? {} : { runtime }),
...(runtime === undefined ? {} : { runtime, colorScheme: defaults.appearance }),
},
});
return mapAtomCommandResult(result, (snapshot) => {
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/preview/openPreviewSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ export async function openPreviewSession<E>(
...(input.url === undefined ? {} : { url: input.url }),
viewport: input.viewport ?? browserDefaultOpenViewport(defaults),
profileId: input.profileId ?? browserDefaultOpenProfileId(defaults),
...(runtime === undefined || runtime === "desktop" ? {} : { runtime }),
...(runtime === undefined || runtime === "desktop"
? {}
: { runtime, colorScheme: defaults.appearance }),
},
});
if (result._tag === "Failure") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export async function openTerminalLinkInPreview<E>(
url: input.url,
viewport: browserDefaultOpenViewport(defaults),
profileId: browserDefaultOpenProfileId(defaults),
...(runtime === undefined ? {} : { runtime }),
...(runtime === undefined ? {} : { runtime, colorScheme: defaults.appearance }),
},
});
if (result._tag === "Failure") {
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ export const PreviewOpenInput = Schema.Struct({
viewport: Schema.optional(PreviewViewportSetting),
profileId: Schema.optional(BrowserProfileId),
runtime: Schema.optional(PreviewRuntime),
/** Server tabs only; the desktop seeds its own tabs at creation. */
colorScheme: Schema.optional(PreviewAppearancePreference),
reveal: Schema.optional(Schema.Boolean),
});
export type PreviewOpenInput = typeof PreviewOpenInput.Type;
Expand Down
Loading