mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-29 17:09:40 +08:00
fix(app): open Review pane for browser focus in background sessions (#50284)
This commit is contained in:
@@ -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<Record<string, BrowserAttachment | undefined>>({})
|
||||
// Servers whose plugin lacks the browser RPC; sessions on them stop retrying.
|
||||
const [unsupported, setUnsupported] = createStore<Record<string, true | undefined>>({})
|
||||
const live = new Map<string, Live>()
|
||||
const focus = new Map<string, Set<(tabID: Browser.TabID) => void>>()
|
||||
const preview = new Map<string, Set<(path: string) => void>>()
|
||||
const key = (server: Server, sessionID: string) => `${server.key}\n${sessionID}`
|
||||
const subscribe = <T>(
|
||||
listeners: Map<string, Set<(value: T) => 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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -137,6 +137,7 @@ export function useSessionModel() {
|
||||
tabs: layout.tabs,
|
||||
view: layout.view,
|
||||
tabKey: layout.tabKey,
|
||||
sessionKey: layout.sessionKey,
|
||||
},
|
||||
ownership: createSessionOwnership(layout.sessionKey),
|
||||
tabs: {
|
||||
|
||||
Reference in New Issue
Block a user