|
7 | 7 | */ |
8 | 8 | import type { |
9 | 9 | DesktopPreviewAnnotationTheme, |
| 10 | + DesktopPreviewColorScheme, |
10 | 11 | DesktopPreviewPointerEvent, |
11 | 12 | PreviewAnnotationPayload, |
12 | 13 | PreviewAnnotationRect, |
@@ -84,6 +85,7 @@ export interface PreviewTabState { |
84 | 85 | canGoBack: boolean; |
85 | 86 | canGoForward: boolean; |
86 | 87 | zoomFactor: number; |
| 88 | + colorScheme: DesktopPreviewColorScheme; |
87 | 89 | controller: "human" | "agent" | "none"; |
88 | 90 | updatedAt: string; |
89 | 91 | } |
@@ -1288,6 +1290,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
1288 | 1290 | canGoBack: false, |
1289 | 1291 | canGoForward: false, |
1290 | 1292 | zoomFactor: DEFAULT_ZOOM_FACTOR, |
| 1293 | + colorScheme: "system", |
1291 | 1294 | controller: "none", |
1292 | 1295 | updatedAt, |
1293 | 1296 | }; |
@@ -1320,6 +1323,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
1320 | 1323 | canGoBack: false, |
1321 | 1324 | canGoForward: false, |
1322 | 1325 | zoomFactor: DEFAULT_ZOOM_FACTOR, |
| 1326 | + colorScheme: "system", |
1323 | 1327 | controller: "none", |
1324 | 1328 | updatedAt, |
1325 | 1329 | }; |
@@ -1386,7 +1390,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
1386 | 1390 | wc.getZoomFactor(), |
1387 | 1391 | ); |
1388 | 1392 | yield* attachListeners(tabId, wc); |
1389 | | - runFork(ensureControlSession(wc).pipe(Effect.ignore)); |
| 1393 | + runFork(restoreControlSession(tabId, wc)); |
1390 | 1394 | const registeredAt = yield* currentIso; |
1391 | 1395 | const registration = yield* SynchronizedRef.modify(tabsRef, (tabs) => { |
1392 | 1396 | const current = tabs.get(tabId); |
@@ -1457,6 +1461,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
1457 | 1461 | canGoBack: current?.canGoBack ?? false, |
1458 | 1462 | canGoForward: current?.canGoForward ?? false, |
1459 | 1463 | zoomFactor: current?.zoomFactor ?? DEFAULT_ZOOM_FACTOR, |
| 1464 | + colorScheme: current?.colorScheme ?? "system", |
1460 | 1465 | controller: current?.controller ?? "none", |
1461 | 1466 | updatedAt, |
1462 | 1467 | }; |
@@ -1525,7 +1530,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
1525 | 1530 | yield* detachControlSession(wc.id); |
1526 | 1531 | yield* attempt({ operation: "openDevTools", tabId, webContentsId: wc.id }, () => { |
1527 | 1532 | wc.once("devtools-closed", () => { |
1528 | | - if (!wc.isDestroyed()) runFork(ensureControlSession(wc).pipe(Effect.ignore)); |
| 1533 | + if (!wc.isDestroyed()) runFork(restoreControlSession(tabId, wc)); |
1529 | 1534 | }); |
1530 | 1535 | wc.openDevTools({ mode: "detach" }); |
1531 | 1536 | }); |
@@ -1684,6 +1689,65 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
1684 | 1689 | yield* update(tabId, { zoomFactor: next }); |
1685 | 1690 | }); |
1686 | 1691 |
|
| 1692 | + // Emulated media lives on the CDP debugger session, not the WebContents, so |
| 1693 | + // it is lost whenever the session detaches (webview swap, DevTools |
| 1694 | + // open/close) and must be re-applied after every (re)attach. |
| 1695 | + const applyColorScheme = Effect.fn("PreviewManager.applyColorScheme")(function* ( |
| 1696 | + tabId: string, |
| 1697 | + wc: Electron.WebContents, |
| 1698 | + colorScheme: DesktopPreviewColorScheme, |
| 1699 | + ) { |
| 1700 | + yield* ensureControlSession(wc); |
| 1701 | + yield* attemptPromise({ operation: "applyColorScheme", tabId, webContentsId: wc.id }, () => |
| 1702 | + wc.debugger.sendCommand("Emulation.setEmulatedMedia", { |
| 1703 | + features: [ |
| 1704 | + { |
| 1705 | + name: "prefers-color-scheme", |
| 1706 | + // An empty value clears the override so the page follows the OS. |
| 1707 | + value: colorScheme === "system" ? "" : colorScheme, |
| 1708 | + }, |
| 1709 | + ], |
| 1710 | + }), |
| 1711 | + ); |
| 1712 | + }); |
| 1713 | + |
| 1714 | + // Re-establish the control session after a detach, restoring any |
| 1715 | + // color-scheme override the tab carries. The scheme is read after the |
| 1716 | + // session attaches so a concurrent setColorScheme is not overwritten with |
| 1717 | + // a stale snapshot. |
| 1718 | + const restoreControlSession = (tabId: string, wc: Electron.WebContents) => |
| 1719 | + ensureControlSession(wc).pipe( |
| 1720 | + Effect.andThen(SynchronizedRef.get(tabsRef)), |
| 1721 | + Effect.flatMap((tabs) => { |
| 1722 | + const colorScheme = tabs.get(tabId)?.colorScheme ?? "system"; |
| 1723 | + return colorScheme === "system" ? Effect.void : applyColorScheme(tabId, wc, colorScheme); |
| 1724 | + }), |
| 1725 | + Effect.ignore, |
| 1726 | + ); |
| 1727 | + |
| 1728 | + const setColorScheme = Effect.fn("PreviewManager.setColorScheme")(function* ( |
| 1729 | + tabId: string, |
| 1730 | + colorScheme: DesktopPreviewColorScheme, |
| 1731 | + ) { |
| 1732 | + const tab = (yield* SynchronizedRef.get(tabsRef)).get(tabId); |
| 1733 | + if (!tab) { |
| 1734 | + return yield* new PreviewTabNotFoundError({ tabId }); |
| 1735 | + } |
| 1736 | + if (tab.colorScheme !== colorScheme) { |
| 1737 | + // Record the choice even when the CDP call below can't run yet (no |
| 1738 | + // webview, DevTools holding the debugger) — it is re-applied on the |
| 1739 | + // next control-session (re)attach. |
| 1740 | + yield* update(tabId, { colorScheme }); |
| 1741 | + } |
| 1742 | + // Re-read after the update: registerWebview may have swapped the guest |
| 1743 | + // in the meantime and the override must land on the current one. |
| 1744 | + const webContentsId = (yield* SynchronizedRef.get(tabsRef)).get(tabId)?.webContentsId; |
| 1745 | + if (webContentsId == null) return; |
| 1746 | + const wc = webContents.fromId(webContentsId); |
| 1747 | + if (!wc || wc.isDestroyed()) return; |
| 1748 | + yield* applyColorScheme(tabId, wc, colorScheme); |
| 1749 | + }); |
| 1750 | + |
1687 | 1751 | const captureScreenshot = Effect.fn("PreviewManager.captureScreenshot")(function* ( |
1688 | 1752 | tabId: string, |
1689 | 1753 | ) { |
@@ -2526,6 +2590,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
2526 | 2590 | revealArtifact, |
2527 | 2591 | saveRecording, |
2528 | 2592 | setAnnotationTheme, |
| 2593 | + setColorScheme, |
2529 | 2594 | setMainWindow, |
2530 | 2595 | startRecording, |
2531 | 2596 | stopRecording, |
@@ -2830,6 +2895,10 @@ export class PreviewManager extends Context.Service< |
2830 | 2895 | readonly zoomOut: (tabId: string) => Effect.Effect<void, PreviewManagerError>; |
2831 | 2896 | readonly resetZoom: (tabId: string) => Effect.Effect<void, PreviewManagerError>; |
2832 | 2897 | readonly hardReload: (tabId: string) => Effect.Effect<void, PreviewManagerError>; |
| 2898 | + readonly setColorScheme: ( |
| 2899 | + tabId: string, |
| 2900 | + colorScheme: DesktopPreviewColorScheme, |
| 2901 | + ) => Effect.Effect<void, PreviewManagerError>; |
2833 | 2902 | readonly openDevTools: (tabId: string) => Effect.Effect<void, PreviewManagerError>; |
2834 | 2903 | readonly clearCookies: () => Effect.Effect<void, PreviewManagerError>; |
2835 | 2904 | readonly clearCache: () => Effect.Effect<void, PreviewManagerError>; |
@@ -2921,6 +2990,7 @@ export const make = Effect.gen(function* PreviewManagerMake() { |
2921 | 2990 | zoomOut: operations.zoomOut, |
2922 | 2991 | resetZoom: operations.resetZoom, |
2923 | 2992 | hardReload: operations.hardReload, |
| 2993 | + setColorScheme: operations.setColorScheme, |
2924 | 2994 | openDevTools: operations.openDevTools, |
2925 | 2995 | clearCookies: Effect.fn("PreviewManager.clearCookies")(function* () { |
2926 | 2996 | yield* browserSession |
|
0 commit comments