diff --git a/apps/desktop/src/app/chat/pane-mirror.ts b/apps/desktop/src/app/chat/pane-mirror.ts index a2525048b4..c0886ff771 100644 --- a/apps/desktop/src/app/chat/pane-mirror.ts +++ b/apps/desktop/src/app/chat/pane-mirror.ts @@ -10,6 +10,7 @@ import type { ReadableAtom } from 'nanostores' import type { ReactElement, ReactNode, PointerEvent as ReactPointerEvent } from 'react' import { registerPaneCloser, removeTreePane, treePanesWithPrefix } from '@/components/pane-shell/tree/store' +import type { MenuKit } from '@/components/ui/actions-menu' import { registry } from '@/contrib/registry' import type { WorkspaceMode } from '@/contrib/types' import type { TileDock } from '@/store/session-states' @@ -52,6 +53,8 @@ export interface PaneMirror { * Per tile so a mirror can offer it for some of its tabs and not others. */ newTab?: (key: string) => (() => void) | undefined render: (key: string) => ReactNode + /** Extra rows at the top of the zone tab menu (see PaneChrome.tabMenuPrefix). */ + tabMenuPrefix?: (key: string) => ((kit: MenuKit) => ReactNode) | undefined /** Wrap the tile's TAB (domain context menu — session verbs). */ tabWrap?: (key: string, tab: ReactElement) => ReactNode /** Override the tile's TAB drag (session drop language: stack/split/link). @@ -113,6 +116,7 @@ export function paneMirror(cfg: PaneMirror): () => void { tabDrag: cfg.tabDrag ? (event: ReactPointerEvent, onTap: () => void) => cfg.tabDrag!(key, event, onTap) : undefined, // returns boolean (handled) — see PaneChrome.tabDrag + tabMenuPrefix: cfg.tabMenuPrefix?.(key), tabWrap: cfg.tabWrap ? (tab: ReactElement) => cfg.tabWrap!(key, tab) : undefined }, render: () => cfg.render(key), diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/track-model.ts b/apps/desktop/src/components/pane-shell/tree/renderer/track-model.ts index 9d28bb262c..1a0dacc023 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/track-model.ts +++ b/apps/desktop/src/components/pane-shell/tree/renderer/track-model.ts @@ -9,6 +9,7 @@ import type * as React from 'react' +import type { MenuKit } from '@/components/ui/actions-menu' import type { Contribution } from '@/contrib/types' import type { GroupNode, LayoutNode } from '../model' @@ -75,6 +76,10 @@ interface PaneChrome extends PaneSizing { * pin/branch/rename/archive/delete). The wrapper must render `tab` as its * interactive child; the zone's own strip menu still owns non-tab space. */ tabWrap?: (tab: React.ReactElement) => React.ReactNode + /** Extra rows at the top of the zone tab menu. Called when the menu opens + * against the right-clicked pane — a Browser tab's Open-in-external, without + * replacing Reload / Close / the strip. */ + tabMenuPrefix?: (kit: MenuKit) => React.ReactNode /** Override this pane's TAB drag (a session tab drags like a sidebar row — * stack / split / composer-link — not the generic pane move). Given the * tab's tap (activate) so that gesture survives. Returns whether it took the diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx index dece540d59..57e57e4d8c 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx +++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx @@ -97,6 +97,7 @@ function ZoneMenu({ minimized, nodeId, stripVisible, + tabMenuPrefix, targetPane }: { children: ReactNode @@ -111,6 +112,8 @@ function ZoneMenu({ /** Whether the strip is on screen — the Hide/Show row toggles against what * the user can see, not against the stored mode (a zone on auto has none). */ stripVisible?: boolean + /** Domain verbs for the right-clicked pane, resolved when the menu opens. */ + tabMenuPrefix?: (kit: MenuKit) => ReactNode /** The right-clicked chip (else the active pane) — what the close-others / * to-the-right / all verbs measure from. Called when the menu RENDERS, not * on every zone re-render: resolving the siblings reads the layout tree, @@ -131,8 +134,12 @@ function ZoneMenu({ const paneId = closable?.() const targetId = targetPane() + const prefix = tabMenuPrefix?.(kit) + return ( <> + {prefix} + {prefix ? : null} {renderActionItem(kit, { icon: 'refresh', label: t.zones.reload, @@ -394,6 +401,7 @@ export function TreeGroup({ minimized: node.minimized, nodeId: node.id, stripVisible, + tabMenuPrefix: (kit: MenuKit) => paneChrome(paneFor(targetPane())).tabMenuPrefix?.(kit), targetPane }