Skip to content

Commit 09ebad8

Browse files
feat(web): the desktop app renders its own server's browser tabs natively
Server tabs of the environment the desktop app launched mount as a <webview> created with their server tab key, so the desktop offers the page to the server instead of streaming it back as JPEG frames. Every other client and environment keeps streaming. The server reports these tabs' navigation itself, so the desktop stops reporting it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent f890280 commit 09ebad8

8 files changed

Lines changed: 85 additions & 15 deletions

File tree

‎apps/web/src/browser/ElectronBrowserHost.tsx‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,37 @@
22

33
import { parseScopedThreadKey } from "@t3tools/client-runtime/environment";
44
import { FILL_PREVIEW_VIEWPORT } from "@t3tools/contracts";
5+
import { useAtomValue } from "@effect/atom-react";
56
import { useEffect, useMemo } from "react";
67

8+
import { primaryEnvironmentIdAtom } from "~/state/primaryEnvironment";
9+
710
import { isElectron } from "~/env";
811
import { useTheme } from "~/hooks/useTheme";
912
import { useActivePreviewSessions } from "~/previewStateStore";
1013

1114
import { readPreviewAnnotationTheme } from "./annotationTheme";
1215
import { useBrowserPointerStore } from "./browserPointerStore";
1316
import { HostedBrowserWebview } from "./HostedBrowserWebview";
17+
import { rendersServerTabNatively } from "./previewRuntime";
1418
import { previewRuntimeTabId } from "./previewRuntimeTabId";
1519

1620
export function ElectronBrowserHost() {
1721
const { resolvedTheme } = useTheme();
1822
const previewByThreadKey = useActivePreviewSessions();
23+
const primaryEnvironmentId = useAtomValue(primaryEnvironmentIdAtom);
1924
const sessions = useMemo(
2025
() =>
2126
Object.entries(previewByThreadKey).flatMap(([threadKey, previewState]) => {
2227
const threadRef = parseScopedThreadKey(threadKey);
23-
// Server tabs stream from the environment and never get a local webview.
28+
// Server tabs of other environments stream; this desktop's own server tabs render here.
2429
return threadRef
2530
? Object.values(previewState.sessions)
26-
.filter((snapshot) => snapshot.runtime !== "server")
31+
.filter(
32+
(snapshot) =>
33+
snapshot.runtime !== "server" ||
34+
rendersServerTabNatively(threadRef.environmentId, primaryEnvironmentId, snapshot),
35+
)
2736
.map((snapshot) => ({
2837
threadRef,
2938
snapshot,
@@ -38,7 +47,7 @@ export function ElectronBrowserHost() {
3847
}))
3948
: [];
4049
}),
41-
[previewByThreadKey],
50+
[previewByThreadKey, primaryEnvironmentId],
4251
);
4352

4453
useEffect(() => {
@@ -98,6 +107,7 @@ export function ElectronBrowserHost() {
98107
pictureInPicture={pictureInPicture}
99108
profileId={snapshot.profileId}
100109
zoomFactor={zoomFactor}
110+
serverDriven={snapshot.runtime === "server"}
101111
/>
102112
);
103113
})}

‎apps/web/src/browser/HostedBrowserWebview.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ describe("HostedBrowserWebview settings hydration", () => {
174174
await retry;
175175
});
176176

177-
expect(acquire).toHaveBeenCalledExactlyOnceWith(runtimeTabId);
177+
expect(acquire).toHaveBeenCalledExactlyOnceWith(runtimeTabId, undefined);
178178
expect(mocks.getPreviewConfig).toHaveBeenCalledExactlyOnceWith(threadRef.environmentId, "work");
179179
expect(createGuest).toHaveBeenCalledOnce();
180180
expect(createGuest).toHaveBeenCalledWith(

‎apps/web/src/browser/HostedBrowserWebview.tsx‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export function HostedBrowserWebview(props: {
5656
*/
5757
readonly profileId: string | undefined;
5858
readonly zoomFactor: number;
59+
/** A tab of the desktop's own server; the server drives this webview's page. */
60+
readonly serverDriven?: boolean;
5961
}) {
6062
const {
6163
threadRef,
@@ -66,6 +68,7 @@ export function HostedBrowserWebview(props: {
6668
pictureInPicture,
6769
zoomFactor,
6870
profileId,
71+
serverDriven = false,
6972
} = props;
7073
const clientSettingsHydrated = useClientSettingsHydrated();
7174
const config = usePreviewWebviewConfig(threadRef.environmentId, profileId);
@@ -93,18 +96,21 @@ export function HostedBrowserWebview(props: {
9396
(state) => (state.activityByTabId[runtimeTabId] ?? 0) > 0,
9497
);
9598
const recordingActive = useActiveBrowserRecordingTabIds().has(runtimeTabId);
96-
usePreviewBridge({ threadRef, tabId, runtimeTabId });
99+
usePreviewBridge({ threadRef, tabId, runtimeTabId, serverDriven });
97100

98101
useEffect(() => {
99102
if (!clientSettingsHydrated) return;
100103
crashRecoveryRef.current = INITIAL_WEBVIEW_CRASH_RECOVERY_STATE;
101-
const lease = acquireDesktopTab(runtimeTabId);
104+
const lease = acquireDesktopTab(
105+
runtimeTabId,
106+
serverDriven ? { threadId: threadRef.threadId, tabId } : undefined,
107+
);
102108
tabLeaseRef.current = lease;
103109
return () => {
104110
if (tabLeaseRef.current === lease) tabLeaseRef.current = null;
105111
lease.release();
106112
};
107-
}, [clientSettingsHydrated, runtimeTabId]);
113+
}, [clientSettingsHydrated, runtimeTabId, serverDriven, tabId, threadRef.threadId]);
108114

109115
const [webviewGeneration, setWebviewGeneration] = useState(0);
110116
const [recoverySrc, setRecoverySrc] = useState(initialSrc);

‎apps/web/src/browser/desktopTabLifetime.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ export interface AcquiredDesktopTab {
3636
readonly release: () => void;
3737
}
3838

39-
export function acquireDesktopTab(tabId: string): AcquiredDesktopTab {
39+
/** `serverTab` names the server tab this desktop tab renders, so the server can drive it. */
40+
export function acquireDesktopTab(
41+
tabId: string,
42+
serverTab?: { readonly threadId: string; readonly tabId: string },
43+
): AcquiredDesktopTab {
4044
const current =
4145
leases.get(tabId) ??
4246
({
@@ -45,7 +49,10 @@ export function acquireDesktopTab(tabId: string): AcquiredDesktopTab {
4549
// Zoom/appearance defaults travel with creation so the guest never
4650
// paints a frame at 100%/system before the preference is applied.
4751
ready: enqueueDesktopTabOperation(tabId, async () =>
48-
previewBridge?.createTab(tabId, browserDefaultTabState(await resolveBrowserDefaults())),
52+
previewBridge?.createTab(tabId, {
53+
...browserDefaultTabState(await resolveBrowserDefaults()),
54+
...(serverTab === undefined ? {} : { serverTab }),
55+
}),
4956
),
5057
} satisfies DesktopTabLease);
5158
if (current.closeTimer !== null) window.clearTimeout(current.closeTimer);

‎apps/web/src/browser/previewRuntime.ts‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import type { EnvironmentId, PreviewRuntime } from "@t3tools/contracts";
1+
import { useAtomValue } from "@effect/atom-react";
2+
import type { EnvironmentId, PreviewRuntime, PreviewSessionSnapshot } from "@t3tools/contracts";
23

4+
import { isElectron } from "~/env";
35
import { isPreviewSupportedInRuntime } from "~/previewStateStore";
6+
import { appAtomRegistry } from "~/rpc/atomRegistry";
7+
import { primaryEnvironmentIdAtom } from "~/state/primaryEnvironment";
48
import {
59
readEnvironmentSupportsServerBrowser,
610
useEnvironmentSupportsServerBrowser,
@@ -19,3 +23,39 @@ export function usePreviewAvailable(environmentId: EnvironmentId | null): boolea
1923
const serverBrowser = useEnvironmentSupportsServerBrowser(environmentId);
2024
return isPreviewSupportedInRuntime() || serverBrowser;
2125
}
26+
27+
/**
28+
* Whether this client draws a server tab with its own `<webview>`. The desktop
29+
* app renders tabs of the server it launched, which drives them over the
30+
* desktop browser channel; every other client and environment streams them.
31+
*/
32+
export function rendersServerTabNatively(
33+
environmentId: EnvironmentId,
34+
primaryEnvironmentId: EnvironmentId | null,
35+
snapshot: Pick<PreviewSessionSnapshot, "runtime"> | null | undefined,
36+
): boolean {
37+
return (
38+
isElectron &&
39+
snapshot?.runtime === "server" &&
40+
primaryEnvironmentId !== null &&
41+
environmentId === primaryEnvironmentId
42+
);
43+
}
44+
45+
export function readRendersServerTabNatively(
46+
environmentId: EnvironmentId,
47+
snapshot: Pick<PreviewSessionSnapshot, "runtime"> | null | undefined,
48+
): boolean {
49+
return rendersServerTabNatively(
50+
environmentId,
51+
appAtomRegistry.get(primaryEnvironmentIdAtom),
52+
snapshot,
53+
);
54+
}
55+
56+
export function useRendersServerTabNatively(
57+
environmentId: EnvironmentId,
58+
snapshot: Pick<PreviewSessionSnapshot, "runtime"> | null | undefined,
59+
): boolean {
60+
return rendersServerTabNatively(environmentId, useAtomValue(primaryEnvironmentIdAtom), snapshot);
61+
}

‎apps/web/src/components/preview/PreviewView.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { revealInFileExplorerLabel } from "./fileExplorerLabel";
5959
import { shouldShowPreviewEmptyState } from "./previewEmptyStateLogic";
6060
import { Badge } from "~/components/ui/badge";
6161
import { BrowserSurfaceSlot } from "~/browser/BrowserSurfaceSlot";
62+
import { useRendersServerTabNatively } from "~/browser/previewRuntime";
6263
import { ServerBrowserSurface, type ServerBrowserHandle } from "~/browser/ServerBrowserSurface";
6364
import { cn } from "~/lib/utils";
6465
import { useBrowserSurfaceStore } from "~/browser/browserSurfaceStore";
@@ -152,8 +153,10 @@ export function PreviewView({
152153
: findActiveBrowserRecordingRuntimeTabId(threadRef, tabId)
153154
: null;
154155
const snapshot = tabId ? (previewState.sessions[tabId] ?? null) : null;
155-
// Server tabs run in the environment's own browser and stream to any client.
156-
const isServerTab = snapshot?.runtime === "server";
156+
// Server tabs run in the environment's browser and stream to any client, except the
157+
// desktop app's own server's tabs, which render here natively while the server drives them.
158+
const nativeServerTab = useRendersServerTabNatively(threadRef.environmentId, snapshot);
159+
const isServerTab = snapshot?.runtime === "server" && !nativeServerTab;
157160
const serverSurfaceRef = useRef<ServerBrowserHandle | null>(null);
158161
const [serverControlledTabId, setServerControlledTabId] = useState<string | null>(null);
159162
const serverInputDisabled = isServerTab && serverControlledTabId !== runtimeTabId;

‎apps/web/src/components/preview/ThreadPreviewMiniPlayer.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import { useBrowserSurfaceStore } from "~/browser/browserSurfaceStore";
2222
import type { BrowserViewportResizeDirection } from "~/browser/browserViewportLayout";
2323
import { previewRuntimeTabId } from "~/browser/previewRuntimeTabId";
24+
import { useRendersServerTabNatively } from "~/browser/previewRuntime";
2425
import { type ServerBrowserHandle, ServerBrowserSurface } from "~/browser/ServerBrowserSurface";
2526
import {
2627
closeServerPictureInPicture,
@@ -127,7 +128,8 @@ function BrowserMiniPlayer({ threadRef, tabId, miniPlayer }: Props & { readonly
127128
const fittedSourceContent = useBrowserSurfaceStore(
128129
(state) => state.byTabId[runtimeTabId]?.fittedSourceContent ?? null,
129130
);
130-
const serverTab = snapshot?.runtime === "server";
131+
const nativeServerTab = useRendersServerTabNatively(threadRef.environmentId, snapshot);
132+
const serverTab = snapshot?.runtime === "server" && !nativeServerTab;
131133
const [streamViewport, setStreamViewport] = useState<PreviewStreamViewport | null>(null);
132134
const serverSurfaceRef = useRef<ServerBrowserHandle | null>(null);
133135
const serverPictureInPicture =

‎apps/web/src/components/preview/usePreviewBridge.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ export function usePreviewBridge(input: {
3939
threadRef: ScopedThreadRef;
4040
tabId: string;
4141
runtimeTabId: string;
42+
/** The server drives this tab and reports its navigation itself. */
43+
serverDriven?: boolean;
4244
}): void {
43-
const { threadRef, tabId, runtimeTabId } = input;
45+
const { threadRef, tabId, runtimeTabId, serverDriven = false } = input;
4446
const clearBrowserPointer = useBrowserPointerStore((state) => state.clear);
4547
const reportStatus = useAtomCommand(previewEnvironment.reportStatus, "preview status report");
4648
const bridge = previewBridge;
@@ -79,7 +81,7 @@ export function usePreviewBridge(input: {
7981
lastReportedUrl: lastReportedUrl.current,
8082
lastReportedKind: lastReportedKind.current,
8183
});
82-
if (!reported) return;
84+
if (!reported || serverDriven) return;
8385
lastReportedUrl.current = reported.lastReportedUrl;
8486
lastReportedKind.current = reported.lastReportedKind;
8587
void reportStatus({

0 commit comments

Comments
 (0)