From 7c65c21a20c2a56a2382121401776e2c2e579b00 Mon Sep 17 00:00:00 2001 From: PR Batch Tester Date: Thu, 22 Oct 2026 14:21:25 +0000 Subject: [PATCH 1/2] fix(desktop): agents see visible:true while the desktop shows their tab The server counted a tab as visible only when a client streamed its frames. The desktop draws server tabs in its own webview and never streams them, so preview_status and tabs[].visible said false while the tab was on screen, and tabs picked without a tabId fell back to recency. The desktop renderer now reports when a server-driven webview is on screen (Browser panel or picture-in-picture). Electron's main process sends that over the existing desktop browser channel as a "shown" event, re-sends it when the backend reconnects or the tab attaches again, and clears it when the tab's webview is destroyed. The server counts a shown, attached desktop tab as visible. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/src/ipc/channels.ts | 1 + apps/desktop/src/ipc/methods/preview.ts | 11 +++++ apps/desktop/src/preload.ts | 2 + .../src/preview/DesktopBrowserHost.test.ts | 21 +++++++++ .../desktop/src/preview/DesktopBrowserHost.ts | 22 +++++++++- apps/desktop/src/preview/Manager.ts | 20 +++++++++ .../src/preview/DesktopBrowserChannel.ts | 12 +++++ apps/server/src/preview/ServerBrowser.test.ts | 44 +++++++++++++++++++ apps/server/src/preview/ServerBrowser.ts | 12 ++--- apps/web/src/browser/HostedBrowserWebview.tsx | 15 ++++++- packages/contracts/src/desktopBrowser.ts | 2 + packages/contracts/src/ipc.ts | 7 +++ 12 files changed, 162 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/ipc/channels.ts b/apps/desktop/src/ipc/channels.ts index 538dd06b0b42a..fb718ee230240 100644 --- a/apps/desktop/src/ipc/channels.ts +++ b/apps/desktop/src/ipc/channels.ts @@ -89,6 +89,7 @@ export const PREVIEW_SET_ZOOM_FACTOR_CHANNEL = "desktop:preview-set-zoom-factor" export const PREVIEW_HARD_RELOAD_CHANNEL = "desktop:preview-hard-reload"; export const PREVIEW_SET_COLOR_SCHEME_CHANNEL = "desktop:preview-set-color-scheme"; export const PREVIEW_SET_AUDIO_MUTED_CHANNEL = "desktop:preview-set-audio-muted"; +export const PREVIEW_SET_SHOWN_CHANNEL = "desktop:preview-set-shown"; export const PREVIEW_OPEN_DEVTOOLS_CHANNEL = "desktop:preview-open-devtools"; export const PREVIEW_CLEAR_COOKIES_CHANNEL = "desktop:preview-clear-cookies"; export const PREVIEW_CLEAR_CACHE_CHANNEL = "desktop:preview-clear-cache"; diff --git a/apps/desktop/src/ipc/methods/preview.ts b/apps/desktop/src/ipc/methods/preview.ts index 20b7bb4877d59..c4c33f6b8dca8 100644 --- a/apps/desktop/src/ipc/methods/preview.ts +++ b/apps/desktop/src/ipc/methods/preview.ts @@ -9,6 +9,7 @@ import { DesktopPreviewRegisterWebviewInputSchema, DesktopPreviewScreenshotArtifactSchema, DesktopPreviewSetAudioMutedInputSchema, + DesktopPreviewSetShownInputSchema, DesktopPreviewSetColorSchemeInputSchema, DesktopPreviewSetZoomFactorInputSchema, BrowserImportResult, @@ -193,6 +194,15 @@ const setAudioMuted = DesktopIpc.makeIpcMethod({ yield* manager.setAudioMuted(tabId, audioMuted); }), }); +const setShown = DesktopIpc.makeIpcMethod({ + channel: IpcChannels.PREVIEW_SET_SHOWN_CHANNEL, + payload: DesktopPreviewSetShownInputSchema, + result: Schema.Void, + handler: Effect.fn("desktop.ipc.preview.setShown")(function* ({ tabId, shown }) { + const manager = yield* PreviewManager.PreviewManager; + yield* manager.setShown(tabId, shown); + }), +}); const openDevTools = tabMethod( IpcChannels.PREVIEW_OPEN_DEVTOOLS_CHANNEL, "desktop.ipc.preview.openDevTools", @@ -432,6 +442,7 @@ export const methods = [ setColorScheme, setZoomFactor, setAudioMuted, + setShown, openDevTools, clearCookies, clearCache, diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 950ed972c6453..94d45c0a0e29a 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -347,6 +347,8 @@ contextBridge.exposeInMainWorld("desktopBridge", { ipcRenderer.invoke(IpcChannels.PREVIEW_SET_COLOR_SCHEME_CHANNEL, { tabId, colorScheme }), setAudioMuted: (tabId, audioMuted) => ipcRenderer.invoke(IpcChannels.PREVIEW_SET_AUDIO_MUTED_CHANNEL, { tabId, audioMuted }), + setShown: (tabId, shown) => + ipcRenderer.invoke(IpcChannels.PREVIEW_SET_SHOWN_CHANNEL, { tabId, shown }), openDevTools: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_OPEN_DEVTOOLS_CHANNEL, { tabId }), listBrowserImportSources: () => ipcRenderer.invoke(IpcChannels.PREVIEW_IMPORT_SOURCES_CHANNEL), diff --git a/apps/desktop/src/preview/DesktopBrowserHost.test.ts b/apps/desktop/src/preview/DesktopBrowserHost.test.ts index 08fefd8d733f5..84d34d0cd32bd 100644 --- a/apps/desktop/src/preview/DesktopBrowserHost.test.ts +++ b/apps/desktop/src/preview/DesktopBrowserHost.test.ts @@ -144,6 +144,27 @@ describe("DesktopBrowserHost", () => { }), ); + it.effect("re-sends shown to a backend that starts later and when a tab attaches again", () => + Effect.gen(function* () { + const host = yield* DesktopBrowserHost.make; + host.attach(key, makeDebuggee().tab); + host.setShown(key, true); + const reader = yield* takeEvents(host, 5).pipe(Effect.forkScoped); + + yield* Effect.yieldNow; + host.detach(key); + host.attach(key, makeDebuggee().tab); + + expect(yield* Fiber.join(reader)).toEqual([ + { type: "attached", ...key }, + { type: "shown", ...key, shown: true }, + { type: "detached", ...key }, + { type: "attached", ...key }, + { type: "shown", ...key, shown: true }, + ]); + }), + ); + it.effect("drops replies from a relay the server released", () => Effect.gen(function* () { const host = yield* DesktopBrowserHost.make; diff --git a/apps/desktop/src/preview/DesktopBrowserHost.ts b/apps/desktop/src/preview/DesktopBrowserHost.ts index 30f252e3a677e..ee8ef95fcc176 100644 --- a/apps/desktop/src/preview/DesktopBrowserHost.ts +++ b/apps/desktop/src/preview/DesktopBrowserHost.ts @@ -87,6 +87,7 @@ export class DesktopBrowserHost extends Context.Service< readonly handleCommandLine: (line: string) => Effect.Effect; readonly attach: (key: DesktopBrowserTabKey, debuggee: DesktopBrowserTabDebugger) => void; readonly detach: (key: DesktopBrowserTabKey) => void; + readonly setShown: (key: DesktopBrowserTabKey, shown: boolean) => void; readonly humanStartedDownload: (source: Electron.WebContents) => boolean; readonly placeDownload: (source: Electron.WebContents, item: Electron.DownloadItem) => boolean; readonly pointers: Stream.Stream<{ @@ -119,6 +120,9 @@ export const make = Effect.gen(function* () { const tabs = new Map(); /** Threads whose agent turn runs, by the server's account. */ const agentTurns = new Set(); + // Kept apart from attachment: the renderer can show a webview before its debugger attaches, + // and the server forgets `shown` on detach, for example while DevTools holds the debugger. + const shownTabs = new Set(); const emit = (event: DesktopBrowserEventType) => runFork(PubSub.publish(outbox, event)); const nextScreenshot = (tab: AttachedTab) => { @@ -440,6 +444,15 @@ export const make = Effect.gen(function* () { debuggee.debugger.on("message", tab.onMessage); debuggee.webContents.on("did-navigate", tab.onNewDocument); emit({ type: "attached", ...key }); + if (shownTabs.has(id)) emit({ type: "shown", ...key, shown: true }); + }; + + const setShown = (key: DesktopBrowserTabKey, shown: boolean) => { + const id = keyOf(key); + if (shownTabs.has(id) === shown) return; + if (shown) shownTabs.add(id); + else shownTabs.delete(id); + if (tabs.has(id)) emit({ type: "shown", ...key, shown }); }; const handleCommandLine = (line: string) => @@ -478,7 +491,13 @@ export const make = Effect.gen(function* () { [...tabs.values()], (tab) => { dropRelay(tab); - return PubSub.publish(outbox, { type: "attached", ...tab.key }); + return PubSub.publish(outbox, { type: "attached", ...tab.key }).pipe( + Effect.andThen( + shownTabs.has(keyOf(tab.key)) + ? PubSub.publish(outbox, { type: "shown", ...tab.key, shown: true }) + : Effect.void, + ), + ); }, { discard: true }, ); @@ -498,6 +517,7 @@ export const make = Effect.gen(function* () { handleCommandLine, attach, detach, + setShown, placeDownload, humanStartedDownload, }); diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts index 036c0d4afd932..8bd8ac6f14e7f 100644 --- a/apps/desktop/src/preview/Manager.ts +++ b/apps/desktop/src/preview/Manager.ts @@ -1676,6 +1676,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function }); if (Option.isNone(tab)) return; const closedTab = tab.value; + if (closedTab.serverTab) browserHost.setShown(closedTab.serverTab, false); if (closedTab.webContentsId != null) { yield* Effect.all( [ @@ -1884,6 +1885,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function yield* rendererHistory.register(wc, { surface: "preview" }); const webContentsId = wc.id; wc.once("destroyed", () => { + runFork(hideDestroyedGuest(webContentsId)); runFork(detachControlSession(webContentsId)); }); yield* ensureControlSession(wc).pipe( @@ -2337,6 +2339,21 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function yield* applyColorScheme(tabId, wc, colorScheme); }); + const setShown = Effect.fn("PreviewManager.setShown")(function* (tabId: string, shown: boolean) { + const serverTab = (yield* SynchronizedRef.get(tabsRef)).get(tabId)?.serverTab; + if (serverTab) browserHost.setShown(serverTab, shown); + }); + + // A window reload or guest crash destroys the guest without the renderer reporting it hidden. + // The renderer reports again once a new guest registers. + const hideDestroyedGuest = Effect.fnUntraced(function* (webContentsId: number) { + for (const tab of (yield* SynchronizedRef.get(tabsRef)).values()) { + if (tab.webContentsId === webContentsId && tab.serverTab) { + browserHost.setShown(tab.serverTab, false); + } + } + }); + const setAudioMuted = Effect.fn("PreviewManager.setAudioMuted")(function* ( tabId: string, audioMuted: boolean, @@ -3298,6 +3315,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function setAnnotationTheme, setAnnotationSendEnabled, setAudioMuted, + setShown, setColorScheme, setMainWindow, setForwardedShortcuts: (shortcuts: ReadonlyArray) => @@ -3503,6 +3521,7 @@ export class PreviewManager extends Context.Service< tabId: string, audioMuted: boolean, ) => Effect.Effect; + readonly setShown: (tabId: string, shown: boolean) => Effect.Effect; readonly openDevTools: (tabId: string) => Effect.Effect; readonly clearCookies: ( partitions?: ReadonlyArray, @@ -3643,6 +3662,7 @@ export const make = Effect.gen(function* PreviewManagerMake() { setColorScheme: operations.setColorScheme, setZoomFactor: operations.setZoomFactor, setAudioMuted: operations.setAudioMuted, + setShown: operations.setShown, openDevTools: operations.openDevTools, clearCookies: Effect.fn("PreviewManager.clearCookies")(function* (partitions) { yield* browserSession diff --git a/apps/server/src/preview/DesktopBrowserChannel.ts b/apps/server/src/preview/DesktopBrowserChannel.ts index d407fc78faf57..9805d0d77acfe 100644 --- a/apps/server/src/preview/DesktopBrowserChannel.ts +++ b/apps/server/src/preview/DesktopBrowserChannel.ts @@ -44,6 +44,8 @@ export class DesktopBrowserChannel extends Context.Service< readonly detached: Stream.Stream; readonly attached: Stream.Stream; readonly isAttached: (key: DesktopTabKey) => Effect.Effect; + /** Whether a desktop window draws the tab on screen, which no streamed viewer counts. */ + readonly isShown: (key: DesktopTabKey) => boolean; readonly endpoint: (key: DesktopTabKey) => Effect.Effect; readonly pointer: ( key: DesktopTabKey, @@ -60,6 +62,8 @@ const make = Effect.gen(function* () { const controlFd = config.desktopBrowserControlFd; const changes = yield* PubSub.unbounded<{ key: DesktopTabKey; attached: boolean }>(); const attachedTabs = new Set(); + // Only attached tabs count: the desktop re-sends `shown` when a tab attaches again. + const shownTabs = new Set(); const inbound = new Map>(); const writeLock = yield* Semaphore.make(1); @@ -83,6 +87,7 @@ const make = Effect.gen(function* () { pointer: Effect.fn("DesktopBrowserChannel.pointer")(function () { return Effect.void; }), + isShown: () => false, }); } @@ -123,14 +128,20 @@ const make = Effect.gen(function* () { return PubSub.publish(changes, { key, attached: true }); case "detached": { attachedTabs.delete(id); + shownTabs.delete(id); const queue = inbound.get(id); return (queue ? Queue.shutdown(queue) : Effect.void).pipe( Effect.andThen(PubSub.publish(changes, { key, attached: false })), ); } + case "shown": + if (event.shown) shownTabs.add(id); + else shownTabs.delete(id); + return Effect.void; } }), Effect.catchCause((cause) => Effect.logWarning("desktop browser channel stopped", { cause })), + Effect.ensuring(Effect.sync(() => shownTabs.clear())), Effect.forkScoped, ); @@ -221,6 +232,7 @@ const make = Effect.gen(function* () { isAttached: Effect.fn("DesktopBrowserChannel.isAttached")(function (key) { return Effect.sync(() => attachedTabs.has(keyOf(key))); }), + isShown: (key) => shownTabs.has(keyOf(key)), endpoint, turn: Effect.fn("DesktopBrowserChannel.turn")(function (threadId, active) { return command({ type: "turn", threadId, active }); diff --git a/apps/server/src/preview/ServerBrowser.test.ts b/apps/server/src/preview/ServerBrowser.test.ts index 99df18c10cf5a..0eb58f664321c 100644 --- a/apps/server/src/preview/ServerBrowser.test.ts +++ b/apps/server/src/preview/ServerBrowser.test.ts @@ -277,6 +277,7 @@ const desktopRenders = (tabId: string) => { } return desktopTabs.has(tabId); }; +const shownDesktopTabs = new Set(); const releasedDesktopTabs: Array = []; const desktopTurns = new NodeEvents.EventEmitter(); const desktopConnections: Array<{ endpoint: string; context: ReturnType }> = []; @@ -334,6 +335,7 @@ const dependencies = Layer.mergeAll( ), ), isAttached: (key) => Effect.sync(() => desktopRenders(key.tabId)), + isShown: (key) => shownDesktopTabs.has(key.tabId), endpoint: (key) => Effect.acquireRelease(Effect.succeed(`ws://desktop/${key.tabId}`), () => Effect.sync(() => releasedDesktopTabs.push(key.tabId)), @@ -380,6 +382,7 @@ beforeEach(() => { focusedFrame = null; desktopTabs.clear(); desktopRendersNext = false; + shownDesktopTabs.clear(); releasedDesktopTabs.length = 0; desktopConnections.length = 0; }); @@ -1144,6 +1147,47 @@ it.live("only the current controller can answer a file picker", () => ).pipe(Effect.provide(layer)), ); +it.live("the tab the desktop shows is visible and picked for agents without a tab", () => + Effect.scoped( + Effect.gen(function* () { + const broker = yield* Broker.PreviewAutomationBroker; + const manager = yield* Manager.PreviewManager; + yield* ServerBrowser.ServerBrowser; + yield* Effect.yieldNow; + const openDesktopTab = Effect.fn("openDesktopTab")(function* (url: string) { + desktopRendersNext = true; + const opened = yield* manager.open({ + threadId: scope.thread.threadId, + url, + runtime: "server", + }); + const tabId = PreviewTabId.make(opened.tabId); + yield* broker.invoke({ scope, tabId, operation: "status", input: {} }); + return tabId; + }); + const recentTab = yield* openDesktopTab("http://localhost:5173/recent"); + const shownTab = yield* openDesktopTab("http://localhost:5173/shown"); + // Opened first and used last, so recency or insertion order alone picks it. + yield* broker.invoke({ scope, tabId: recentTab, operation: "status", input: {} }); + + shownDesktopTabs.add(shownTab); + const status = yield* broker.invoke({ + scope: asSession("agent-b"), + operation: "status", + input: {}, + }); + expect(status).toMatchObject({ + tabId: shownTab, + visible: true, + tabs: [ + expect.objectContaining({ tabId: recentTab, visible: false }), + expect.objectContaining({ tabId: shownTab, visible: true }), + ], + }); + }), + ).pipe(Effect.provide(layer)), +); + it.live("agents read a human's tab with no arguments and act on it only while nobody drives", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index 6f3e86a7501ea..bf99618041116 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -466,6 +466,9 @@ const make = Effect.gen(function* () { const tabs = new Map(); const pendingTabs = new Map>(); + // The desktop draws its tabs in its own webview, so they never get a streamed viewer. + const isVisible = (tab: ServerTab) => + tab.viewers.size > 0 || (tab.desktop !== null && desktopChannel.isShown(tab)); const closedPendingTabs = new Set(); const adoptedPages = new Map(); @@ -597,7 +600,7 @@ const make = Effect.gen(function* () { liveTabs: [...tabs.values()].map((tab) => ({ threadId: tab.threadId, tabId: tab.tabId, - visible: tab.viewers.size > 0, + visible: isVisible(tab), })), }), ), @@ -1307,8 +1310,7 @@ const make = Effect.gen(function* () { .sort((left, right) => right.createdAt - left.createdAt)[0] ?? threadTabs.sort( (left, right) => - Number(right.viewers.size > 0) - Number(left.viewers.size > 0) || - right.usedAt - left.usedAt, + Number(isVisible(right)) - Number(isVisible(left)) || right.usedAt - left.usedAt, )[0] ); }; @@ -1386,7 +1388,7 @@ const make = Effect.gen(function* () { const viewport = tab.page.viewportSize(); const status = { available: true, - visible: tab.viewers.size > 0, + visible: isVisible(tab), tabId: tab.tabId, url: url === "about:blank" ? null : url, title: null, @@ -1410,7 +1412,7 @@ const make = Effect.gen(function* () { ...(candidate.openerTabId === undefined ? {} : { openerTabId: candidate.openerTabId }), owner: tabOwner(candidate), ownedByCaller: candidate.control.agentId === agentSessionId, - visible: candidate.viewers.size > 0, + visible: isVisible(candidate), ...(candidate.profileId === undefined ? {} : { profileId: candidate.profileId }), })), ...profileStatus(), diff --git a/apps/web/src/browser/HostedBrowserWebview.tsx b/apps/web/src/browser/HostedBrowserWebview.tsx index 70eef380dbff9..c68c5ee99e295 100644 --- a/apps/web/src/browser/HostedBrowserWebview.tsx +++ b/apps/web/src/browser/HostedBrowserWebview.tsx @@ -135,6 +135,9 @@ export function HostedBrowserWebview(props: { const [webviewGeneration, setWebviewGeneration] = useState(0); const [recoverySrc, setRecoverySrc] = useState(initialSrc); + const active = presentation.visible && presentation.rect !== null; + const shown = active || pictureInPicture; + const reportedShown = useEffectEvent(() => serverDriven && shown); const remountCrashedWebview = useEffectEvent(() => { setRecoverySrc(initialUrl ?? initialSrc); setWebviewGeneration((generation) => generation + 1); @@ -162,6 +165,9 @@ export function HostedBrowserWebview(props: { const webContentsId = webview.getWebContentsId(); if (Number.isInteger(webContentsId) && webContentsId > 0) { await bridge.registerWebview(runtimeTabId, webContentsId); + if (reportedShown()) { + withDesktopTab(runtimeTabId, () => bridge.setShown(runtimeTabId, true)); + } } } catch {} })(); @@ -203,7 +209,6 @@ export function HostedBrowserWebview(props: { webviewGeneration, ]); - const active = presentation.visible && presentation.rect !== null; const lastRect = presentation.rect; const layoutZoomFactor = useBrowserLayoutZoomFactor(zoomFactor); const viewportWidth = viewport._tag === "fill" ? null : viewport.width; @@ -289,6 +294,14 @@ export function HostedBrowserWebview(props: { viewportWidth, ]); + // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- Report the mounted webview's visibility to the desktop host. + useEffect(() => { + const bridge = window.desktopBridge?.preview; + if (!clientSettingsHydrated || !bridge || !serverDriven || !shown) return; + withDesktopTab(runtimeTabId, () => bridge.setShown(runtimeTabId, true)); + return () => withDesktopTab(runtimeTabId, () => bridge.setShown(runtimeTabId, false)); + }, [clientSettingsHydrated, runtimeTabId, serverDriven, shown]); + if (!clientSettingsHydrated || !config) return null; const renderingActive = active || backgroundActivity || pictureInPicture || recordingActive; diff --git a/packages/contracts/src/desktopBrowser.ts b/packages/contracts/src/desktopBrowser.ts index 17d256f0d8d3e..52fb66885adfe 100644 --- a/packages/contracts/src/desktopBrowser.ts +++ b/packages/contracts/src/desktopBrowser.ts @@ -12,6 +12,8 @@ export const DesktopBrowserEvent = Schema.Union([ /** Its `` went away: closed, crashed, swapped, or devtools took the debugger. */ Schema.Struct({ type: Schema.Literal("detached"), ...TabKey }), Schema.Struct({ type: Schema.Literal("cdp"), ...TabKey, message: Schema.String }), + /** Its `` started or stopped being on screen in a desktop window. */ + Schema.Struct({ type: Schema.Literal("shown"), ...TabKey, shown: Schema.Boolean }), ]); export type DesktopBrowserEvent = typeof DesktopBrowserEvent.Type; diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index f14452fec5a92..aeed885d1bab9 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -989,6 +989,11 @@ export const DesktopPreviewSetAudioMutedInputSchema = Schema.Struct({ audioMuted: Schema.Boolean, }); +export const DesktopPreviewSetShownInputSchema = Schema.Struct({ + tabId: DesktopPreviewTabIdSchema, + shown: Schema.Boolean, +}); + export const DesktopPreviewAnnotationThemeInputSchema = Schema.Struct({ theme: DesktopPreviewAnnotationThemeSchema, }); @@ -1152,6 +1157,8 @@ export interface DesktopPreviewBridge { hardReload: (tabId: string) => Promise; setColorScheme: (tabId: string, colorScheme: DesktopPreviewColorScheme) => Promise; setAudioMuted: (tabId: string, audioMuted: boolean) => Promise; + /** Tells the tab's server whether its webview is on screen. */ + setShown: (tabId: string, shown: boolean) => Promise; openDevTools: (tabId: string) => Promise; clearCookies: (environmentId: EnvironmentId, profileId?: string) => Promise; clearCache: (environmentId: EnvironmentId, profileId?: string) => Promise; From 54b3735a3ad646def5b6b51c42133b36f4efd089 Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:12:31 +0000 Subject: [PATCH 2/2] fix(server): keep live-tab host ranking on stream viewers Co-Authored-By: Claude Opus 5.5 --- apps/server/src/preview/ServerBrowser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index 2ac07339af5fe..7c8a002b3a8ec 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -630,7 +630,7 @@ const make = Effect.gen(function* () { liveTabs: [...tabs.values()].map((tab) => ({ threadId: tab.threadId, tabId: tab.tabId, - visible: isVisible(tab), + visible: tab.viewers.size > 0, })), }), ),