Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions apps/server/src/preview/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export class PreviewManager extends Context.Service<
readonly reportStatus: (
input: PreviewReportStatusInput & { readonly serverControlled?: boolean },
) => Effect.Effect<void, PreviewError>;
/** Records that a server tab runs headless. It stays headless until it closes. */
readonly markHeadless: (
input: PreviewCloseInput & { readonly tabId: string },
) => Effect.Effect<void, PreviewError>;
readonly requestReveal: (
input: PreviewCloseInput & { readonly tabId: string; readonly force: boolean },
) => Effect.Effect<void, PreviewError>;
Expand Down Expand Up @@ -307,6 +311,34 @@ export const make = Effect.gen(function* PreviewManagerMake() {
);
});

const markHeadless: PreviewManager["Service"]["markHeadless"] = Effect.fn(
"PreviewManager.markHeadless",
)(function* (input) {
return yield* mutateExistingSession(
input.threadId,
input.tabId,
Effect.fn("PreviewManager.markSessionHeadless")(function* (session) {
if (session.snapshot.headless) return { next: session, emit: null, result: undefined };
const snapshot: PreviewSessionSnapshot = {
...session.snapshot,
headless: true,
updatedAt: yield* currentIsoTimestamp,
};
return {
next: { ...session, snapshot },
emit: {
type: "resized",
threadId: session.threadId,
tabId: session.tabId,
createdAt: snapshot.updatedAt,
snapshot,
},
result: undefined,
};
}),
);
});

const resize: PreviewManager["Service"]["resize"] = Effect.fn("PreviewManager.resize")(
function* (input) {
return yield* mutateExistingSession(
Expand Down Expand Up @@ -469,6 +501,7 @@ export const make = Effect.gen(function* PreviewManagerMake() {
requestReveal,
navigate,
reportStatus,
markHeadless,
resize,
adjust,
refresh,
Expand Down
21 changes: 21 additions & 0 deletions apps/server/src/preview/ServerBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,27 @@ it.live.each(["desktop" as const, undefined])(
).pipe(Effect.provide(layer)),
);

it.effect("a tab the desktop does not draw is marked headless, so the desktop streams it", () =>
Effect.scoped(
Effect.gen(function* () {
const { broker, tabId } = yield* ready;
const manager = yield* Manager.PreviewManager;
desktopRendersNext = true;
const desktopTab = yield* broker.invoke<PreviewAutomationStatus>({
scope,
operation: "open",
input: { reuseExistingTab: false, show: false },
});
const { sessions } = yield* manager.list({ threadId: scope.thread.threadId });
const headlessOf = (id: string | null) =>
sessions.find((session) => session.tabId === id)?.headless;

expect(headlessOf(tabId)).toBe(true);
expect(headlessOf(desktopTab.tabId)).toBeUndefined();
}),
).pipe(Effect.provide(layer)),
);

it.live.each([
{ method: "goto" as const, message: { type: "navigate", url: "http://10.255.255.1/" } },
{ method: "goBack" as const, message: { type: "history", delta: -1 } },
Expand Down
10 changes: 9 additions & 1 deletion apps/server/src/preview/ServerBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ const make = Effect.gen(function* () {
};

const desktopRenders = (snapshot: PreviewSessionSnapshot) =>
desktopChannel.available
desktopChannel.available && !snapshot.headless
? runPromise(
desktopChannel.awaitAttached(
{ threadId: snapshot.threadId, tabId: snapshot.tabId },
Expand Down Expand Up @@ -715,6 +715,14 @@ const make = Effect.gen(function* () {
adopted === undefined && (await desktopRenders(snapshot))
? await connectDesktop(snapshot)
: null;
// Otherwise a desktop that attaches late shows its own page, and the agent drives another.
if (!desktop && desktopChannel.available) {
await runPromise(
manager
.markHeadless({ threadId: ThreadId.make(snapshot.threadId), tabId: snapshot.tabId })
.pipe(Effect.ignore),
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const isolatedContext =
adopted === undefined &&
desktop === null &&
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/browser/previewRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,20 @@ export function usePreviewAvailable(environmentId: EnvironmentId | null): boolea
export function rendersServerTabNatively(
environmentId: EnvironmentId,
primaryEnvironmentId: EnvironmentId | null,
snapshot: Pick<PreviewSessionSnapshot, "runtime"> | null | undefined,
snapshot: Pick<PreviewSessionSnapshot, "runtime" | "headless"> | null | undefined,
): boolean {
return (
isElectron &&
snapshot?.runtime === "server" &&
!snapshot.headless &&
primaryEnvironmentId !== null &&
environmentId === primaryEnvironmentId
);
}

export function useRendersServerTabNatively(
environmentId: EnvironmentId,
snapshot: Pick<PreviewSessionSnapshot, "runtime"> | null | undefined,
snapshot: Pick<PreviewSessionSnapshot, "runtime" | "headless"> | null | undefined,
): boolean {
return rendersServerTabNatively(environmentId, useAtomValue(primaryEnvironmentIdAtom), snapshot);
}
4 changes: 3 additions & 1 deletion apps/web/src/components/preview/PreviewView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ export function PreviewView({
const [serverControlledTabId, setServerControlledTabId] = useState<string | null>(null);
const serverInputDisabled = isServerTab && serverControlledTabId !== runtimeTabId;
const [serverFrameTabId, setServerFrameTabId] = useState<string | null>(null);
const desktopOverlay = tabId ? (previewState.desktopByTabId[tabId] ?? null) : null;
// A desktop page that mounted before its tab went headless leaves its stale state here.
const desktopOverlay =
tabId && !isServerTab ? (previewState.desktopByTabId[tabId] ?? null) : null;
const layoutZoomFactor = useBrowserLayoutZoomFactor(desktopOverlay?.zoomFactor ?? 1);
const navStatus = snapshot?.navStatus ?? { _tag: "Idle" as const };
const url = navStatus._tag === "Idle" ? "" : navStatus.url;
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 @@ -160,6 +160,8 @@ export const PreviewSessionSnapshot = Schema.Struct({
/** Fixed at open; Electron cannot change a webview partition after attach without losing page state. */
profileId: Schema.optional(BrowserProfileId),
runtime: Schema.optional(PreviewRuntime),
/** The server runs this tab in headless Chromium, so the desktop streams it like other clients. */
headless: Schema.optional(Schema.Boolean),
automationOwner: Schema.optional(Schema.String),
reveal: Schema.optional(Schema.Boolean),
revealRequest: Schema.optional(Schema.Struct({ id: Schema.String, force: Schema.Boolean })),
Expand Down
Loading