From 6770bc37178757f1b54cb019025df8a88c778b75 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 26 Aug 2026 20:32:12 +0300 Subject: [PATCH] fix(browser): keep the panel closed when an agent opens a page The agent's browser.open used to force the context panel open and steal the active surface, which read as panels opening by themselves. Tab upserts now take a reveal option: the agent's opener passes reveal: false, so the tab mounts invisibly (panes are kept alive regardless of visibility, so agent control still works) while the panel and the active tab stay exactly as the user left them. Manual opens are unchanged. --- CHANGELOG.md | 1 + .../ui/src/components/layout/ContextPanel.tsx | 7 ++++-- packages/ui/src/stores/useUIStore.ts | 24 ++++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd021143..f03d79e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ All notable changes to this project will be documented in this file. - Chat: a "Follow new content while streaming" checkbox (Settings → Chat → Streaming, on by default) turns automatic following off entirely; with it off, the scroll-to-bottom pill now appears as soon as the reply grows past the visible area. - Command palette: rarely used commands (pin session, copy session ID, multi-run launcher, archived sessions, notes, todos, status, theme) are found by typing but stay off the first screen. - Mobile: narrowing a browser window past phone size switches into the mobile layout (and back when widened); the old/new mobile layout setting is gone. +- Browser: an agent opening a page with the browser tool no longer pops the browser panel open (or switches the surface you're on) — the page loads in the background and the rail is where you peek at it. - Usage: the Command Code tile is gone — their official API exposes no usage data, so the tile could only fail. - Auth: an expired OpenChamber login is announced within seconds by a banner with a Log in button, instead of being discovered through failing actions. Sending pauses until login, and a conversation that failed to load reloads itself afterwards. - Chat: a failed send returns your typed prompt to the input — whatever the reason — instead of losing it to an error toast; a mid-send session switch lands it in that session's draft. diff --git a/packages/ui/src/components/layout/ContextPanel.tsx b/packages/ui/src/components/layout/ContextPanel.tsx index 8c24e134..aa52be5d 100644 --- a/packages/ui/src/components/layout/ContextPanel.tsx +++ b/packages/ui/src/components/layout/ContextPanel.tsx @@ -452,10 +452,13 @@ export const ContextPanel: React.FC = () => { // Lets an agent's browser.open create the tab it needs when none is open yet. // Registered from the panel because opening a tab is panel state, not - // something the browser view itself can do before it exists. + // something the browser view itself can do before it exists. Background on + // purpose: an agent working a page must not pop the panel open (or steal + // the active surface) under the user — the tab mounts invisibly, and the + // rail is where the user opens it when curious. React.useEffect(() => { if (!effectiveDirectory) return; - return registerBrowserOpener((url) => openContextBrowser(effectiveDirectory, url)); + return registerBrowserOpener((url) => openContextBrowser(effectiveDirectory, url, { reveal: false })); }, [effectiveDirectory, openContextBrowser]); const reorderContextPanelTabs = useUIStore((state) => state.reorderContextPanelTabs); const setSelectedFilePath = useFilesViewTabsStore((state) => state.setSelectedPath); diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index b5254f2e..5eceb243 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -393,7 +393,9 @@ const touchContextPanelState = (prev?: ContextPanelDirectoryState): ContextPanel const upsertContextPanelTab = ( current: ContextPanelDirectoryState, descriptor: ContextPanelTabDescriptor, + options?: { reveal?: boolean }, ): ContextPanelDirectoryState => { + const reveal = options?.reveal !== false; const nextTab = createContextPanelTab(descriptor); // A real file tab replaces the empty editor placeholder ('file' with no // target) that the rail can open before any file is picked. @@ -418,12 +420,18 @@ const upsertContextPanelTab = ( } : tab)); - const activeTabId = nextTab.id; + // A background upsert (an agent working a page) keeps the panel exactly as + // the user left it: closed stays closed, and whatever tab they were on + // stays active. The tab still exists — panes are kept mounted regardless of + // visibility — so agent control and a later manual open both find it. + const activeTabId = reveal + ? nextTab.id + : current.activeTabId ?? nextTab.id; const clampedTabs = clampContextPanelTabs(tabs, CONTEXT_PANEL_MAX_TABS, activeTabId); return { ...current, - isOpen: true, + isOpen: reveal ? true : current.isOpen, tabs: clampedTabs, activeTabId: resolveActiveContextPanelTabID(clampedTabs, activeTabId), touchedAt: Date.now(), @@ -806,14 +814,14 @@ interface UIStore { toggleContextEditorTree: () => void; setContextEditorTreeWidth: (width: number) => void; openContextSurface: (directory: string, mode: ContextPanelMode) => void; - openContextPanelTab: (directory: string, tab: ContextPanelTabDescriptor) => void; + openContextPanelTab: (directory: string, tab: ContextPanelTabDescriptor, options?: { reveal?: boolean }) => void; openContextDiff: (directory: string, filePath: string, staged?: boolean, scope?: PendingDiffScope | null) => void; openContextFile: (directory: string, filePath: string) => void; openContextFileAtLine: (directory: string, filePath: string, line: number, column?: number) => void; openContextOverview: (directory: string) => void; openContextPlan: (directory: string) => void; openContextPreview: (directory: string, url: string) => void; - openContextBrowser: (directory: string, url?: string) => void; + openContextBrowser: (directory: string, url?: string, options?: { reveal?: boolean }) => void; openNewContextBrowserTab: (directory: string) => void; setContextPanelTabTargetPath: (directory: string, tabID: string, targetPath: string) => void; setActiveContextPanelTab: (directory: string, tabID: string) => void; @@ -1239,7 +1247,7 @@ export const useUIStore = create()( state.openContextPanelTab(normalizedDirectory, { mode }); }, - openContextPanelTab: (directory, tab) => { + openContextPanelTab: (directory, tab, options) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); if (!normalizedDirectory) { return; @@ -1250,7 +1258,7 @@ export const useUIStore = create()( const current = touchContextPanelState(prev); const byDirectory = { ...state.contextPanelByDirectory, - [normalizedDirectory]: upsertContextPanelTab(current, tab), + [normalizedDirectory]: upsertContextPanelTab(current, tab, options), }; return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) }; @@ -1351,7 +1359,7 @@ export const useUIStore = create()( label: null, }); }, - openContextBrowser: (directory, url = '') => { + openContextBrowser: (directory, url = '', options) => { const normalizedDirectory = normalizeDirectoryPath((directory || '').trim()); if (!normalizedDirectory || isVSCodeRuntime()) return; const targetUrl = typeof url === 'string' && url.trim().length > 0 ? url.trim() : ''; @@ -1360,7 +1368,7 @@ export const useUIStore = create()( targetPath: targetUrl, dedupeKey: targetUrl || 'browser', label: null, - }); + }, options); }, setContextPanelTabTargetPath: (directory, tabID, targetPath) => {