From 27eef4253fd2f1e08c5752633eef22da1058f7f2 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 12 May 2026 11:13:31 -0700 Subject: [PATCH 1/2] feat(desktop): surface background terminal sessions in v2 tab bar; restore move-to-background button - Add renderTabBarTrailing slot to @superset/panes Workspace/TabBar - New BackgroundTerminalsButton in v2-workspace tab bar: lists daemon terminal sessions with no pane attached; click to re-open as a tab, trash icon to kill - Restore the 'Move terminal to background' archive button in the v2 terminal pane header (removed in #3888) --- .../BackgroundTerminalsButton.tsx | 151 ++++++++++++++++++ .../BackgroundTerminalsButton/index.ts | 1 + .../TerminalHeaderExtras.tsx | 26 +++ .../v2-workspace/$workspaceId/page.tsx | 7 + .../react/components/Workspace/Workspace.tsx | 2 + .../Workspace/components/TabBar/TabBar.tsx | 12 ++ packages/panes/src/react/types.ts | 2 + 7 files changed, 201 insertions(+) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/BackgroundTerminalsButton.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/index.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/BackgroundTerminalsButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/BackgroundTerminalsButton.tsx new file mode 100644 index 00000000000..24ad921c3e3 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/BackgroundTerminalsButton.tsx @@ -0,0 +1,151 @@ +import type { WorkspaceStore } from "@superset/panes"; +import { Button } from "@superset/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@superset/ui/dropdown-menu"; +import { toast } from "@superset/ui/sonner"; +import { workspaceTrpc } from "@superset/workspace-client"; +import { Archive, ChevronDown, Trash2 } from "lucide-react"; +import { useMemo, useState } from "react"; +import { getRelativeTime } from "renderer/screens/main/components/WorkspacesListView/utils"; +import { useStore } from "zustand"; +import type { StoreApi } from "zustand/vanilla"; +import type { PaneViewerData, TerminalPaneData } from "../../types"; + +interface BackgroundTerminalsButtonProps { + workspaceId: string; + store: StoreApi>; +} + +/** + * Tab-bar control that surfaces running terminal daemon sessions for the + * workspace that have no pane attached (e.g. moved to background via the + * terminal pane header). Renders nothing when there are none; otherwise a + * single button with a dropdown to re-open or kill each background session. + */ +export function BackgroundTerminalsButton({ + workspaceId, + store, +}: BackgroundTerminalsButtonProps) { + const [isOpen, setIsOpen] = useState(false); + const tabs = useStore(store, (s) => s.tabs); + const utils = workspaceTrpc.useUtils(); + const killSession = workspaceTrpc.terminal.killSession.useMutation(); + const sessionsQuery = workspaceTrpc.terminal.listSessions.useQuery( + { workspaceId }, + { refetchInterval: isOpen ? 2_000 : 5_000, refetchOnWindowFocus: true }, + ); + + const attachedTerminalIds = useMemo(() => { + const ids = new Set(); + for (const tab of tabs) { + for (const pane of Object.values(tab.panes)) { + if (pane.kind !== "terminal") continue; + const data = pane.data as Partial; + if (data.terminalId) ids.add(data.terminalId); + } + } + return ids; + }, [tabs]); + + const backgroundSessions = useMemo(() => { + const sessions = sessionsQuery.data?.sessions ?? []; + return sessions + .filter((session) => !attachedTerminalIds.has(session.terminalId)) + .sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0)); + }, [sessionsQuery.data?.sessions, attachedTerminalIds]); + + if (backgroundSessions.length === 0) return null; + + const label = `${backgroundSessions.length} background terminal session${ + backgroundSessions.length === 1 ? "" : "s" + }`; + + const handleAdopt = (terminalId: string) => { + store.getState().addTab({ + panes: [ + { + kind: "terminal", + data: { terminalId } as TerminalPaneData, + }, + ], + }); + void utils.terminal.listSessions.invalidate({ workspaceId }); + setIsOpen(false); + }; + + const handleKill = async (terminalId: string) => { + try { + await killSession.mutateAsync({ terminalId, workspaceId }); + } catch (error) { + console.error( + "[BackgroundTerminalsButton] Failed to kill session:", + error, + ); + toast.error("Failed to close terminal session"); + } finally { + void utils.terminal.listSessions.invalidate({ workspaceId }); + } + }; + + return ( + + + + + + + Background terminal sessions + + +
+ {backgroundSessions.map((session) => ( + handleAdopt(session.terminalId)} + > + + + {session.title ?? "Terminal"} + + {session.createdAt > 0 && ( + + {getRelativeTime(session.createdAt, { format: "compact" })} + + )} + + + ))} +
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/index.ts new file mode 100644 index 00000000000..857435ee703 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/index.ts @@ -0,0 +1 @@ +export { BackgroundTerminalsButton } from "./BackgroundTerminalsButton"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalHeaderExtras/TerminalHeaderExtras.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalHeaderExtras/TerminalHeaderExtras.tsx index 73da69c261b..414ceddd2b3 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalHeaderExtras/TerminalHeaderExtras.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalHeaderExtras/TerminalHeaderExtras.tsx @@ -1,4 +1,7 @@ import type { RendererContext } from "@superset/panes"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { Archive } from "lucide-react"; +import { markTerminalForBackground } from "renderer/lib/terminal/terminal-background-intents"; import type { PaneViewerData, TerminalPaneData, @@ -19,6 +22,11 @@ export function TerminalHeaderExtras({ const data = context.pane.data as TerminalPaneData; + const handleMoveToBackground = () => { + markTerminalForBackground(data.terminalId); + void context.actions.close(); + }; + return (
+ + + + + + Move terminal to background + +
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx index c7f0b1a1557..8f140a09782 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx @@ -10,6 +10,7 @@ import { ResizablePanel } from "renderer/screens/main/components/ResizablePanel" import { getV2NotificationSourcesForTab } from "renderer/stores/v2-notifications"; import { useWorkspace } from "../providers/WorkspaceProvider"; import { AddTabMenu } from "./components/AddTabMenu"; +import { BackgroundTerminalsButton } from "./components/BackgroundTerminalsButton"; import { V2NotificationStatusIndicator } from "./components/V2NotificationStatusIndicator"; import { V2PresetsBar } from "./components/V2PresetsBar"; import { V2WorkspaceRunButton } from "./components/V2WorkspaceRunButton"; @@ -275,6 +276,12 @@ function V2WorkspaceContent() { onToggleShowPresetsBar={setShowPresetsBar} /> )} + renderTabBarTrailing={() => ( + + )} renderEmptyState={() => ( ({ renderTabIcon, renderEmptyState, renderAddTabMenu, + renderTabBarTrailing, renderBelowTabBar, onBeforeCloseTab, onAfterCloseTab, @@ -96,6 +97,7 @@ export function Workspace({ } renderTabIcon={renderTabIcon} renderAddTabMenu={renderAddTabMenu} + renderTabBarTrailing={renderTabBarTrailing} renderTabAccessory={renderTabAccessory} /> {renderBelowTabBar?.()} diff --git a/packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx b/packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx index 0c9fd90dccf..4d3c117d869 100644 --- a/packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx +++ b/packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx @@ -33,6 +33,7 @@ interface TabBarProps { onMovePaneToNewTab: (paneId: string, toIndex: number) => void; renderTabIcon?: (tab: Tab) => ReactNode; renderAddTabMenu?: () => ReactNode; + renderTabBarTrailing?: () => ReactNode; renderTabAccessory?: (tab: Tab) => ReactNode; } @@ -82,6 +83,7 @@ export function TabBar({ onMovePaneToNewTab, renderTabIcon, renderAddTabMenu, + renderTabBarTrailing, renderTabAccessory, }: TabBarProps) { const tabsTrackRef = useRef(null); @@ -173,6 +175,11 @@ export function TabBar({
+ {renderTabBarTrailing && ( +
+ {renderTabBarTrailing()} +
+ )}
); } @@ -228,6 +235,11 @@ export function TabBar({ )} + {renderTabBarTrailing && ( +
+ {renderTabBarTrailing()} +
+ )} ); } diff --git a/packages/panes/src/react/types.ts b/packages/panes/src/react/types.ts index c66015d9471..153680d7519 100644 --- a/packages/panes/src/react/types.ts +++ b/packages/panes/src/react/types.ts @@ -105,6 +105,8 @@ export interface WorkspaceProps { renderTabIcon?: (tab: Tab) => ReactNode; renderEmptyState?: () => ReactNode; renderAddTabMenu?: () => ReactNode; + /** Rendered at the trailing (right) edge of the tab bar row. */ + renderTabBarTrailing?: () => ReactNode; renderBelowTabBar?: () => ReactNode; onBeforeClosePane?: ( pane: Pane, From 609c2a484d46c5d42096cf38376a724ad0e183b0 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 12 May 2026 11:45:38 -0700 Subject: [PATCH 2/2] fix(desktop): scope background-session kill button disabled state per row --- .../BackgroundTerminalsButton/BackgroundTerminalsButton.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/BackgroundTerminalsButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/BackgroundTerminalsButton.tsx index 24ad921c3e3..c17c0aa6cf7 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/BackgroundTerminalsButton.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/BackgroundTerminalsButton/BackgroundTerminalsButton.tsx @@ -132,7 +132,10 @@ export function BackgroundTerminalsButton({ type="button" aria-label="Close terminal session" title="Close terminal session" - disabled={killSession.isPending} + disabled={ + killSession.isPending && + killSession.variables?.terminalId === session.terminalId + } className="shrink-0 rounded p-0.5 opacity-0 transition-opacity hover:bg-destructive/10 hover:text-destructive disabled:pointer-events-none disabled:opacity-30 group-hover:opacity-100" onClick={(event) => { event.preventDefault();