From 374140d3a63b8d52c196e89751103cc8fc752482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 7 Aug 2026 11:56:45 +0000 Subject: [PATCH] put the previewed browser tab in the url Co-Authored-By: Claude Opus 5 --- docs/navigation-audit.md | 15 +++- docs/workspace-panel-todo.md | 13 ++- src/apps/officer-web/App.tsx | 1 + .../Dashboard/Browser/BrowserScreen.tsx | 10 ++- .../Screens/Dashboard/Browser/TabList.tsx | 87 ++++++++----------- .../Screens/Dashboard/Browser/TabPreview.tsx | 25 +++++- .../Screens/Dashboard/Browser/shared.ts | 34 ++++++++ 7 files changed, 124 insertions(+), 61 deletions(-) create mode 100644 src/apps/officer-web/Screens/Dashboard/Browser/shared.ts diff --git a/docs/navigation-audit.md b/docs/navigation-audit.md index 8f777b79..a6c2f994 100644 --- a/docs/navigation-audit.md +++ b/docs/navigation-audit.md @@ -39,7 +39,8 @@ That fix is the template for the HIGH items below. **Caveat:** the fix only did was in this list; the Projects feature was removed end to end on 2026-07-30, so the route is gone with it. H3 and navigate-site 22 below still name it — they are the record of work that happened, not of code that exists.) **Flat screens:** `/` `/files` `/music` `/tasks` `/skills` `/processes` `/activity` -`/system-monitor` `/task-logs` `/plans` `/terminal` `/desktop` `/browser` `/code-editor`. +`/system-monitor` `/task-logs` `/plans` `/terminal` `/desktop` `/code-editor`. (`/browser` is a pair now too — +`/browser/:tabId`.) The five settings pages are route **pairs** now, not flat screens — `/settings/{profile,ai,system,integrations,user-management}` plus a `:section` each (M6). There has never been a `/settings/apps`; that entry was wrong when this list was written. @@ -101,7 +102,15 @@ is **one design decision** that cascades across many files: - **Dock / Header active styling** (`Dock.tsx:43,78` · `Header.tsx:53,112`) — destinations are already real ``s; only the *active* class is JS-derived from `useLocation`. Switch to react-router `` and drop the hand-rolled `isActive`. -- **Browser tabs** (`Browser/TabList.tsx:93`) — ephemeral CDP targets (polled, come/go every few seconds); a durable `/browser/:targetId` is dubious. Probably leave as selection. +- ~~**Browser tabs**~~ (`Browser/TabList.tsx:93`) — **done, against this file's own advice.** The objection + was that a CDP target id is ephemeral, so a durable `/browser/:tabId` is dubious. True of *bookmarking*, + and irrelevant to everything else the URL buys: the id was in an onClick closure, three components read a + `BROWSER_SELECTED_TAB` global, and the row could not be cmd-clicked. Staleness is handled where it + actually shows up — the preview now distinguishes "no tab open" from "that tab is no longer attached" + by checking the polled target list, which it gets from the same React Query key the list uses, so it + costs no extra request. En route: the row's Focus and Close buttons were nested *inside* the row + ` - - + + {target.title || 'Untitled'} + {target.url} + +
+ +
- {target.url} - + ))} )} diff --git a/src/apps/officer-web/Screens/Dashboard/Browser/TabPreview.tsx b/src/apps/officer-web/Screens/Dashboard/Browser/TabPreview.tsx index 495c9535..dd2186e1 100644 --- a/src/apps/officer-web/Screens/Dashboard/Browser/TabPreview.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Browser/TabPreview.tsx @@ -1,14 +1,20 @@ import { useState, useCallback } from 'react'; +import { Link } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { Globe, RefreshCw, Send, Terminal, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { useClient } from 'hooks/useClient'; -import { useGlobal } from 'hooks/useGlobal'; +import { useBrowserTargets, useSelectedTabId } from './shared'; export const TabPreview = () => { const client = useClient(); - const [selectedId] = useGlobal('BROWSER_SELECTED_TAB', null); + // Reads the URL itself rather than being told by the list. + const selectedId = useSelectedTabId(); + const { data: targets } = useBrowserTargets(!!selectedId); + // A target id dies with the browser session, so a link or a reload can name a tab that is no longer + // attached. Screenshotting it would 404 every ten seconds; better to say so once. + const isKnownTab = !!selectedId && !!targets?.some((t) => t.id === selectedId); const [navUrl, setNavUrl] = useState(''); const [evalExpr, setEvalExpr] = useState(''); const [evalResult, setEvalResult] = useState(null); @@ -23,7 +29,7 @@ export const TabPreview = () => { } = useQuery({ queryKey: ['browser-screenshot', selectedId], queryFn: () => client.get<{ data: string; format: string }>(`/browser/targets/${selectedId}/screenshot`), - enabled: !!selectedId, + enabled: isKnownTab, refetchInterval: 10000, }); @@ -64,6 +70,19 @@ export const TabPreview = () => { ); } + // Waiting on the list is not the same as knowing the tab is gone, so only say so once it has arrived. + if (targets && !isKnownTab) { + return ( +
+ + That tab is no longer attached + + Back to the tab list + +
+ ); + } + return (
{/* URL bar */} diff --git a/src/apps/officer-web/Screens/Dashboard/Browser/shared.ts b/src/apps/officer-web/Screens/Dashboard/Browser/shared.ts new file mode 100644 index 00000000..add75374 --- /dev/null +++ b/src/apps/officer-web/Screens/Dashboard/Browser/shared.ts @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query'; +import { useParams } from 'react-router'; +import { useClient } from 'hooks/useClient'; + +// Which attached tab is being previewed is `/browser/:tabId`. It used to be a `BROWSER_SELECTED_TAB` +// global read by three components, which meant the address bar said `/browser` no matter what you were +// looking at, and a tab row was a `