diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d286a74f..af78e60c 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -2706,8 +2706,10 @@ export default function ChatView(props: ChatViewProps) { const splitChatFraction = useBrowserPanelStore((store) => store.splitChatFraction); const setSplitChatFraction = useBrowserPanelStore((store) => store.setSplitChatFraction); const browserExpanded = useBrowserPanelStore((store) => store.expanded); + // The preview is a Chromium webview, which only the desktop app can host: a + // web build has no browser to toggle, so it gets no button and no panel. // General chats have no project and therefore no dev server to look at. - const browserAvailable = !isGeneralChatThread; + const browserAvailable = isElectron && !isGeneralChatThread; const browserOpen = browserAvailable && browserPanelState.open; const handleToggleBrowser = useCallback(() => { @@ -6982,7 +6984,10 @@ export default function ChatView(props: ChatViewProps) { {/* The agent's end of the browser is mounted with the thread, not with the panel: a closed panel is a closed panel, not the absence of a browser, and a request for the browser opens it. */} - {browserAvailable && routeThreadRef !== null ? ( + {!isGeneralChatThread && routeThreadRef !== null ? ( + // Mounted on the web build too: the host reads the desktop bridge at + // effect time and refuses to connect without one, so it costs nothing + // there and keeps working if the preload attaches late. // The project is passed alongside the thread because a local draft // thread has no shell to look it up from, and browser approvals are // recorded per project. diff --git a/apps/web/src/components/settings/SettingsPanels.browser.tsx b/apps/web/src/components/settings/SettingsPanels.browser.tsx index 763502e0..b2277199 100644 --- a/apps/web/src/components/settings/SettingsPanels.browser.tsx +++ b/apps/web/src/components/settings/SettingsPanels.browser.tsx @@ -1884,9 +1884,18 @@ describe("GeneralSettingsPanel observability", () => { await expect.element(page.getByText(/Credential configured ยท Claude Max/)).toBeInTheDocument(); await page.getByLabelText("Toggle Claude details").click(); - await expect.element(page.getByText("Chat configured")).toBeInTheDocument(); - await expect.element(page.getByText("Usage verified")).toBeInTheDocument(); - await page.getByText("Advanced: headless chat token").click(); + await expect.element(page.getByText("Chat configured")).toBeVisible(); + await expect.element(page.getByText("Usage verified")).toBeVisible(); + const advancedTokenLabel = page.getByText("Advanced: headless chat token"); + await expect.element(advancedTokenLabel).toBeVisible(); + const advancedTokenToggle = advancedTokenLabel.element().closest("summary"); + // The summary can have stable bounds while its parent is still revealing it. + // Wait for that height transition before sending a pointer click. + const detailsPanel = advancedTokenToggle?.closest('[data-slot="collapsible-panel"]'); + if (!advancedTokenToggle || !detailsPanel) + throw new Error("Claude details panel did not render"); + await Promise.all(detailsPanel.getAnimations().map((animation) => animation.finished)); + await page.elementLocator(advancedTokenToggle).click(); await expect.element(page.getByText(/Optional for remote or headless chat/)).toBeVisible(); });