In-app browser and previews are real layout-tree tabs (#77705)
* feat(desktop): shared pane-strip primitives — one bar, one glyph, one close menu The zone header hand-rolled its tab bar, its close-verb context menu, and the bare-glyph "+" inline; the preview rail kept a second copy of all three. Extract PaneTabStrip (the bar), PaneStripGlyph/PaneStripTool (glyph buttons as data, titlebar-tool style), and paneTabCloseItems (the four close verbs) into the pane-tab primitives, and render the zone header through them. Panes contribute strip glyphs via PaneChrome.stripTools; $stripToolsRevision tells the strip to re-read. * refactor(desktop): preview tabs are layout-tree tiles like session and page tiles The in-app browser / preview rail carried its own tab strip beside the zone's own — a second bar at a different height with its own close menu, label casing, ⌘W rung, and welded to the file browser's zone so ⌘J toggled it away. It predated the layout tree. $previewTabs now mirrors into pane contributions through the same paneMirror session and route tiles use, so a preview tab IS a zone tab: one strip, drag/ stack/split, the shared close verbs, plain ⌘W, its own zone docked beside main. URL tabs are titled Browser (the tab names the surface, not the page); files keep their filename and a file-type lead glyph. Deleted with the rail: the preview pane contribution + PREVIEW_PANE_ID + its visibility binding, the 'preview' placements in the default tree and presets, the ⌘W rail rung, the reveal listener, and the preview.close* i18n keys (copies of zones.*). lone-header now keys on "closeable placement:main" instead of the session-tile: id prefix, so any tile dragged into its own zone keeps its tab. * fix(desktop): preview console/DevTools live on the strip, and DevTools tells the truth The two toggles were titlebar tools — far from the preview they act on and one ambiguous global pair once two previews were open. They're strip glyphs now, contributed per-tab as PaneStripTool data with real tooltips: the console store is cached by tab id so the glyph and the panel read the same logs, and the pane registers a DevTools handle for its tab. DevTools active state was also a lie: it tracked our click handler, so closing the DevTools window itself left the glyph stuck on. The webview's devtools-opened/closed events drive it now. * fix(desktop): ⌘W and ⌃Tab work over preview and page zones The generic tab verbs keyed zone eligibility on the CHAT strip (workspace / session-tile: ids), so a zone holding only a Browser or page tile was invisible to them: ⌃Tab skipped it, and ⌘W fell through the chat rung and emptied the MAIN chat while you were looking at a preview. ⌘1…⌘9 already worked — the verbs disagreed about what counts as a tab strip. New isMainStripPane (any placement:'main' tenant — sessions, pages, previews) drives ⌘W and ⌃Tab; isSessionStripPane keeps gating what it should: where a session may dock (⌘T's anchor, the strip's +). * fix(desktop): preview tab selection follows the tree, not just the reverse openPreview drove tree reveals, but clicking a preview TAB only activated its pane in the tree — $rightRailActiveTabId kept naming the previous tab, so $previewTarget (⌘L quote labels, the titlebar's has-preview state) reported a tab that wasn't on screen. The mirror now also listens tree→store: when the interacted zone's active pane is a preview tile, the store selection follows. Both directions converge on the same id, so no ping-pong. * fix(desktop): session drags land in preview and page zones tileZoneHost replaces chatZonePane: a zone hosting any main tile (a Browser tile, a page) accepts stack and split drops — the known asymmetry where you could drag a preview tab out but never drag a session in. Only a CHAT zone's center is the link-to-composer drop; a preview zone's center stacks, since there's no composer to link to. * chore(desktop): drop the rail's dead multi-close verbs closeActiveRightRailTab / closeOtherRightRailTabs / closeRightRailTabsToRight lost their last callers when ⌘W and the close menu moved to the zone strip's shared rungs; the tests now exercise closeRightRailTab's own fallback behavior directly. * fix(desktop): open_preview lands whenever its session is on screen The preview.open handler honored the event only when its session was the FOCUSED one — but the turn that runs open_preview is usually a tile's session, and by the time the tool fires the user's last click has often parked focus on main (or anywhere else). The tool reported success, the store never wrote, and nothing appeared: an explicit 'open reddit' silently vanished. On-screen is the right bar: honor the open when the session is the primary chat or any open tile, which keeps truly invisible background sessions from yanking the pane (offer, don't hijack) without eating opens the user asked for. * fix(desktop): one Browser — a second URL navigates it, not a second tab Tabs were keyed url:<address>, so every distinct page the agent opened stacked another BROWSER tab — three opens, three Browsers, each titled identically because the tab deliberately names the surface, not the page. The title already said singleton; the key disagreed. URL targets now share one url:browser id: openPreview re-fronts the tab and swaps its target, and the pane rebuilds its webview against the new address. Files and artifacts keep per-identity tabs. Restored storage rekeys old per-address rows and keeps only the most recent.
This commit is contained in:
@@ -11,6 +11,7 @@ import type { ReactElement, ReactNode, PointerEvent as ReactPointerEvent } from
|
||||
|
||||
import type { DoubleTapContext } from '@/components/pane-shell/tree/renderer/drag-session'
|
||||
import { registerPaneCloser, removeTreePane, treePanesWithPrefix } from '@/components/pane-shell/tree/store'
|
||||
import type { PaneStripTool } from '@/components/ui/pane-tab'
|
||||
import { registry } from '@/contrib/registry'
|
||||
import type { TileDock } from '@/store/session-states'
|
||||
|
||||
@@ -35,6 +36,11 @@ export interface PaneMirror<T> {
|
||||
* self-subscribing component (e.g. a session's status dot) so the strip needn't
|
||||
* re-sync on status/color change — only `title` drives re-registration. */
|
||||
tabLead?: (key: string) => ReactNode
|
||||
/** Glyph buttons the tile contributes to the strip, after the last tab (where
|
||||
* "+" sits), while it is the ACTIVE pane — e.g. a preview's console /
|
||||
* DevTools toggles. DATA, not markup: the strip's `PaneStripGlyph` owns the
|
||||
* styling so every glyph on every strip matches. */
|
||||
stripTools?: (key: string) => readonly PaneStripTool[]
|
||||
render: (key: string) => ReactNode
|
||||
/** Wrap the tile's TAB (domain context menu — session verbs). */
|
||||
tabWrap?: (key: string, tab: ReactElement) => ReactNode
|
||||
@@ -76,12 +82,16 @@ export function paneMirror<T>(cfg: PaneMirror<T>): () => void {
|
||||
title,
|
||||
data: {
|
||||
tabLead: cfg.tabLead ? () => cfg.tabLead!(key) : undefined,
|
||||
stripTools: cfg.stripTools ? () => cfg.stripTools!(key) : undefined,
|
||||
dock: {
|
||||
before: cfg.before?.(tile),
|
||||
pane: cfg.anchor?.(tile) ?? 'workspace',
|
||||
pos: cfg.dir?.(tile) ?? 'right'
|
||||
},
|
||||
minWidth: cfg.minWidth,
|
||||
// Every mirrored tile is a full workspace surface docked beside main —
|
||||
// and closeable, which is what keeps its tab when it lands in a zone of
|
||||
// its own (see lone-header.ts).
|
||||
placement: 'main',
|
||||
tabDrag: cfg.tabDrag
|
||||
? (event: ReactPointerEvent<HTMLElement>, onTap: () => void, double?: DoubleTapContext) =>
|
||||
|
||||
Reference in New Issue
Block a user