diff --git a/packages/app/src/session/browser/attachments.ts b/packages/app/src/session/browser/attachments.ts index 05631be04bb..db08d76e397 100644 --- a/packages/app/src/session/browser/attachments.ts +++ b/packages/app/src/session/browser/attachments.ts @@ -1,14 +1,15 @@ -import { createEffect, createMemo, getOwner, on, onCleanup, runWithOwner } from "solid-js" +import { batch, createEffect, createMemo, createRoot, getOwner, on, onCleanup, runWithOwner } from "solid-js" import { createStore, reconcile } from "solid-js/store" import { createSimpleContext } from "@opencode/ui/context" -import type { Browser } from "@opencode/plugin-browser/rpc" import { useLanguage } from "@/runtime/i18n/language" import type { BrowserPaneCommand } from "@/runtime/platform/browser-pane" import { usePlatform } from "@/runtime/platform/platform" import type { useServer } from "@/runtime/server/current" +import type { SessionStateKey } from "@/runtime/server/scope" import { useSettings } from "@/settings/model" import { findSessionTab, tabKey, useTabs } from "@/shell/tabs/tabs" -import { useCurrentRoute } from "@/shell/state/layout" +import { useCurrentRoute, useLayout } from "@/shell/state/layout" +import { sessionBrowserTab } from "@/shell/state/session-tabs" import { createEventListener } from "@solid-primitives/event-listener" import { createBrowserConnection, type BrowserConnectionState } from "./connection" @@ -35,28 +36,15 @@ export const { use: useBrowserAttachments, provider: BrowserAttachmentsProvider const settings = useSettings() const language = useLanguage() const shellTabs = useTabs() + const layout = useLayout() const route = useCurrentRoute() const owner = getOwner() const [store, setStore] = createStore>({}) // Servers whose plugin lacks the browser RPC; sessions on them stop retrying. const [unsupported, setUnsupported] = createStore>({}) const live = new Map() - const focus = new Map void>>() const preview = new Map void>>() const key = (server: Server, sessionID: string) => `${server.key}\n${sessionID}` - const subscribe = ( - listeners: Map void>>, - id: string, - listener: (value: T) => void, - ) => { - const set = listeners.get(id) ?? new Set() - set.add(listener) - listeners.set(id, set) - return () => { - set.delete(listener) - if (!set.size) listeners.delete(id) - } - } const enabled = createMemo( () => !!platform.browserPane && settings.ready() && settings.general.experimentalBrowser(), ) @@ -101,11 +89,14 @@ export const { use: useBrowserAttachments, provider: BrowserAttachmentsProvider enabled, supported: (server: Server) => !unsupported[server.key], state: (server: Server, sessionID: string) => store[key(server, sessionID)], - attach(server: Server, sessionID: string) { + attach(server: Server, sessionID: string, sessionKey: SessionStateKey) { const id = key(server, sessionID) if (live.has(id)) return const pane = platform.browserPane if (!pane || !enabled() || unsupported[server.key] || server.health?.incompatible) return + // Focus requests write to the owning session's layout even while another shell tab is routed, + // so the Review pane and browser tab are already selected when the user returns to it. + const tabs = createRoot((dispose) => ({ dispose, layout: layout.tabs(sessionKey) }), owner) const connection = createBrowserConnection({ pane, // Resolve the current port at every wake, including after sidecar replacement. @@ -114,7 +105,14 @@ export const { use: useBrowserAttachments, provider: BrowserAttachmentsProvider sessionID, endpoint: { ...server.conn.http, url: server.ctx.sdk.url }, }), - focus: (tabID) => focus.get(id)?.forEach((listener) => listener(tabID)), + focus: (tabID) => { + const tab = sessionBrowserTab(tabID) + batch(() => { + shellTabs.setPane(findSessionTab(shellTabs.store, server.key, sessionID), "review", true) + if (!tabs.layout.all().includes(tab)) tabs.layout.setAll([...tabs.layout.all(), tab]) + tabs.layout.setActive(tab) + }) + }, preview: (path) => preview.get(id)?.forEach((listener) => listener(path)), change: (state) => { if (state.error === "browser.pane.unsupported") { @@ -153,15 +151,19 @@ export const { use: useBrowserAttachments, provider: BrowserAttachmentsProvider entry.dispose = () => { unsubscribe?.forEach((dispose) => dispose()) connection.dispose() + tabs.dispose() } }, - /** Desktop focus requests for a mounted session route; nothing is replayed to routes mounted later. */ - onFocus(server: Server, sessionID: string, listener: (tabID: Browser.TabID) => void) { - return subscribe(focus, key(server, sessionID), listener) - }, /** Agent requests to show a file in this session's Review pane. */ onPreview(server: Server, sessionID: string, listener: (path: string) => void) { - return subscribe(preview, key(server, sessionID), listener) + const id = key(server, sessionID) + const listeners = preview.get(id) ?? new Set() + listeners.add(listener) + preview.set(id, listeners) + return () => { + listeners.delete(listener) + if (!listeners.size) preview.delete(id) + } }, command(server: Server, sessionID: string, command: BrowserPaneCommand) { const connection = live.get(key(server, sessionID))?.connection diff --git a/packages/app/src/session/browser/model.ts b/packages/app/src/session/browser/model.ts index 60d2a6debc5..6a4d37e8f01 100644 --- a/packages/app/src/session/browser/model.ts +++ b/packages/app/src/session/browser/model.ts @@ -1,4 +1,4 @@ -import { batch, createEffect, createMemo, on, onCleanup } from "solid-js" +import { batch, createEffect, createMemo, on } from "solid-js" import type { Browser } from "@opencode/plugin-browser/rpc" import { createStore } from "solid-js/store" import { useLanguage } from "@/runtime/i18n/language" @@ -33,13 +33,6 @@ export function createSessionBrowser(session: SessionModel) { attachment()?.browser?.tabs.filter((tab) => session.layout.tabs().all().includes(sessionBrowserTab(tab.id))) ?? [], ) - const focus = (tabID: Browser.TabID) => { - session.layout.view().reviewPanel.open() - const tabs = session.layout.tabs() - const key = sessionBrowserTab(tabID) - if (!tabs.all().includes(key)) tabs.setAll([...tabs.all(), key]) - tabs.setActive(key) - } const command = (command: BrowserPaneCommand) => { const sessionID = session.identity.sessionID() if (!sessionID) return @@ -66,8 +59,7 @@ export function createSessionBrowser(session: SessionModel) { createEffect(() => { const sessionID = session.identity.sessionID() if (!sessionID) return - if (attachments.enabled()) attachments.attach(server, sessionID) - onCleanup(attachments.onFocus(server, sessionID, focus)) + if (attachments.enabled()) attachments.attach(server, sessionID, session.layout.sessionKey()) }) createEffect( on( diff --git a/packages/app/src/session/model.ts b/packages/app/src/session/model.ts index c9ff52b92c1..d7a1239d063 100644 --- a/packages/app/src/session/model.ts +++ b/packages/app/src/session/model.ts @@ -137,6 +137,7 @@ export function useSessionModel() { tabs: layout.tabs, view: layout.view, tabKey: layout.tabKey, + sessionKey: layout.sessionKey, }, ownership: createSessionOwnership(layout.sessionKey), tabs: {