diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildSetupPaneLayout.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildSetupPaneLayout.ts index 6a8abfb8746..8221d5ff994 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildSetupPaneLayout.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/buildSetupPaneLayout.ts @@ -17,7 +17,6 @@ export function buildSetupPaneLayout( const tabId = `tab-${crypto.randomUUID()}`; return { id: tabId, - titleOverride: t.label, createdAt: Date.now(), activePaneId: paneId, layout: { type: "pane" as const, paneId }, @@ -25,6 +24,7 @@ export function buildSetupPaneLayout( [paneId]: { id: paneId, kind: "terminal", + titleOverride: t.label, data: { terminalId: t.id } as TerminalPaneData, }, }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx index 22da979ff64..20fda20fd90 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx @@ -1,4 +1,9 @@ -import type { ContextMenuActionConfig, RendererContext } from "@superset/panes"; +import { + type ContextMenuActionConfig, + type PaneRegistry, + type RendererContext, + resolveTabTitle, +} from "@superset/panes"; import { useMemo } from "react"; import { LuColumns2, @@ -18,7 +23,9 @@ import type { TerminalPaneData, } from "../../types"; -export function useDefaultContextMenuActions(): ContextMenuActionConfig[] { +export function useDefaultContextMenuActions( + paneRegistry: PaneRegistry, +): ContextMenuActionConfig[] { const splitDownShortcut = useHotkeyDisplay("SPLIT_DOWN").text; const splitRightShortcut = useHotkeyDisplay("SPLIT_RIGHT").text; const splitWithChatShortcut = useHotkeyDisplay("SPLIT_WITH_CHAT").text; @@ -115,7 +122,7 @@ export function useDefaultContextMenuActions(): ContextMenuActionConfig[] = otherTabs.map((tab) => ({ key: `move-to-${tab.id}`, - label: tab.titleOverride ?? tab.id, + label: resolveTabTitle(tab, tabs, paneRegistry), onSelect: () => { ctx.store .getState() @@ -154,6 +161,7 @@ export function useDefaultContextMenuActions(): ContextMenuActionConfig, -): string | undefined { - const browser = getSingleBrowserPane(tab); - if (!browser) return undefined; - if (browser.data.pageTitle) return browser.data.pageTitle; - if (browser.data.url && browser.data.url !== "about:blank") { - try { - return new URL(browser.data.url).hostname; - } catch {} - } - return undefined; -} - export function renderBrowserTabIcon(tab: Tab) { const browser = getSingleBrowserPane(tab); if (!browser?.data.faviconUrl) return null; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/index.ts index ac9bea3ed43..e6b7ed03397 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/index.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/index.ts @@ -1,7 +1,6 @@ export { BrowserPane, BrowserPaneToolbar, - getBrowserTabTitle, renderBrowserTabIcon, } from "./BrowserPane"; export { browserRuntimeRegistry } from "./browserRuntimeRegistry"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx index d7373ba2258..5b4b796ff86 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx @@ -120,7 +120,8 @@ export function usePaneRegistry( const name = getFileName(data.filePath); return ; }, - getTitle: (ctx: RendererContext) => { + getTitle: (pane) => getFileName((pane.data as FilePaneData).filePath), + renderTitle: (ctx: RendererContext) => { const data = ctx.pane.data as FilePaneData; const name = getFileName(data.filePath); return ( @@ -262,9 +263,15 @@ export function usePaneRegistry( }, browser: { getIcon: () => , - getTitle: (ctx: RendererContext) => { - const data = ctx.pane.data as BrowserPaneData; - return data.pageTitle || data.url; + getTitle: (pane) => { + const data = pane.data as BrowserPaneData; + if (data.pageTitle) return data.pageTitle; + if (data.url && data.url !== "about:blank") { + try { + return new URL(data.url).host; + } catch {} + } + return "Browser"; }, renderPane: (ctx: RendererContext) => ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts index 87209a159d0..5cea7c54be9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts @@ -10,9 +10,13 @@ import { filterMatchingPresetsForProject } from "shared/preset-project-targeting import type { StoreApi } from "zustand/vanilla"; import type { PaneViewerData, TerminalPaneData } from "../../types"; -function makeTerminalPane(terminalId: string): CreatePaneInput { +function makeTerminalPane( + terminalId: string, + titleOverride?: string, +): CreatePaneInput { return { kind: "terminal", + titleOverride, data: { terminalId } as TerminalPaneData, }; } @@ -84,8 +88,7 @@ export function useV2PresetExecution({ preset.commands[0] as string, ); state.addTab({ - titleOverride: preset.name || "Terminal", - panes: [makeTerminalPane(id)], + panes: [makeTerminalPane(id, preset.name || undefined)], }); break; } @@ -94,16 +97,22 @@ export function useV2PresetExecution({ const ids = await Promise.all( preset.commands.map((cmd) => createSessionWithCommand(cmd)), ); - const panes = ids.map((id) => makeTerminalPane(id)); + const panes = ids.map((id) => + makeTerminalPane(id, preset.name || undefined), + ); state.addTab({ - titleOverride: preset.name || "Terminal", panes: panes.length > 0 ? (panes as [ CreatePaneInput, ...CreatePaneInput[], ]) - : [makeTerminalPane(crypto.randomUUID())], + : [ + makeTerminalPane( + crypto.randomUUID(), + preset.name || undefined, + ), + ], }); break; } @@ -114,8 +123,9 @@ export function useV2PresetExecution({ ); for (let i = 0; i < ids.length; i++) { state.addTab({ - titleOverride: preset.name || "Terminal", - panes: [makeTerminalPane(ids[i] as string)], + panes: [ + makeTerminalPane(ids[i] as string, preset.name || undefined), + ], }); } break; @@ -127,14 +137,13 @@ export function useV2PresetExecution({ ); if (!activeTabId) { state.addTab({ - titleOverride: preset.name || "Terminal", - panes: [makeTerminalPane(id)], + panes: [makeTerminalPane(id, preset.name || undefined)], }); break; } state.addPane({ tabId: activeTabId, - pane: makeTerminalPane(id), + pane: makeTerminalPane(id, preset.name || undefined), }); break; } @@ -144,23 +153,29 @@ export function useV2PresetExecution({ preset.commands.map((cmd) => createSessionWithCommand(cmd)), ); if (!activeTabId) { - const panes = ids.map((id) => makeTerminalPane(id)); + const panes = ids.map((id) => + makeTerminalPane(id, preset.name || undefined), + ); state.addTab({ - titleOverride: preset.name || "Terminal", panes: panes.length > 0 ? (panes as [ CreatePaneInput, ...CreatePaneInput[], ]) - : [makeTerminalPane(crypto.randomUUID())], + : [ + makeTerminalPane( + crypto.randomUUID(), + preset.name || undefined, + ), + ], }); break; } for (const id of ids) { state.addPane({ tabId: activeTabId, - pane: makeTerminalPane(id), + pane: makeTerminalPane(id, preset.name || undefined), }); } break; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts index c791446a404..2de8bc35427 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts @@ -39,7 +39,6 @@ export function useWorkspaceHotkeys({ useHotkey("NEW_GROUP", () => { store.getState().addTab({ - titleOverride: "Terminal", panes: [ { kind: "terminal", @@ -51,14 +50,12 @@ export function useWorkspaceHotkeys({ useHotkey("NEW_CHAT", () => { store.getState().addTab({ - titleOverride: "Chat", panes: [{ kind: "chat", data: { sessionId: null } as ChatPaneData }], }); }); useHotkey("NEW_BROWSER", () => { store.getState().addTab({ - titleOverride: "Browser", panes: [ { kind: "browser", 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 ca7271c5c81..50c115cf8d7 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 @@ -22,10 +22,7 @@ import { WorkspaceNotFoundState } from "./components/WorkspaceNotFoundState"; import { WorkspaceSidebar } from "./components/WorkspaceSidebar"; import { useDefaultContextMenuActions } from "./hooks/useDefaultContextMenuActions"; import { usePaneRegistry } from "./hooks/usePaneRegistry"; -import { - getBrowserTabTitle, - renderBrowserTabIcon, -} from "./hooks/usePaneRegistry/components/BrowserPane"; +import { renderBrowserTabIcon } from "./hooks/usePaneRegistry/components/BrowserPane"; import { useV2PresetExecution } from "./hooks/useV2PresetExecution"; import { useV2WorkspacePaneLayout } from "./hooks/useV2WorkspacePaneLayout"; import { useWorkspaceHotkeys } from "./hooks/useWorkspaceHotkeys"; @@ -93,7 +90,7 @@ function WorkspaceContent({ projectId, }); const paneRegistry = usePaneRegistry(workspaceId); - const defaultContextMenuActions = useDefaultContextMenuActions(); + const defaultContextMenuActions = useDefaultContextMenuActions(paneRegistry); const selectedFilePath = useStore(store, (s) => { const tab = s.tabs.find((t) => t.id === s.activeTabId); @@ -108,7 +105,6 @@ function WorkspaceContent({ const state = store.getState(); if (openInNewTab) { state.addTab({ - titleOverride: filePath.split(/[/\\]/).pop(), panes: [ { kind: "file", @@ -139,7 +135,6 @@ function WorkspaceContent({ hasChanges: false, } as FilePaneData, }, - tabTitle: "Files", }); }, [store], @@ -148,9 +143,8 @@ function WorkspaceContent({ const openDiffPane = useCallback( (filePath: string) => { const state = store.getState(); - const activeTab = state.tabs.find((t) => t.id === state.activeTabId); - if (activeTab) { - for (const pane of Object.values(activeTab.panes)) { + for (const tab of state.tabs) { + for (const pane of Object.values(tab.panes)) { if (pane.kind !== "diff") continue; const prev = pane.data as DiffPaneData; state.setPaneData({ @@ -160,19 +154,21 @@ function WorkspaceContent({ path: filePath, } as PaneViewerData, }); - state.setActivePane({ tabId: activeTab.id, paneId: pane.id }); + state.setActiveTab(tab.id); + state.setActivePane({ tabId: tab.id, paneId: pane.id }); return; } } - state.openPane({ - pane: { - kind: "diff", - data: { - path: filePath, - collapsedFiles: [], - } as DiffPaneData, - }, - tabTitle: "Changes", + state.addTab({ + panes: [ + { + kind: "diff", + data: { + path: filePath, + collapsedFiles: [], + } as DiffPaneData, + }, + ], }); }, [store], @@ -180,7 +176,6 @@ function WorkspaceContent({ const addTerminalTab = useCallback(() => { store.getState().addTab({ - titleOverride: "Terminal", panes: [ { kind: "terminal", @@ -194,7 +189,6 @@ function WorkspaceContent({ const addChatTab = useCallback(() => { store.getState().addTab({ - titleOverride: "Chat", panes: [ { kind: "chat", @@ -206,7 +200,6 @@ function WorkspaceContent({ const addBrowserTab = useCallback(() => { store.getState().addTab({ - titleOverride: "Browser", panes: [ { kind: "browser", @@ -270,7 +263,6 @@ function WorkspaceContent({ registry={paneRegistry} paneActions={defaultPaneActions} contextMenuActions={defaultContextMenuActions} - getTabTitle={getBrowserTabTitle} renderTabIcon={renderBrowserTabIcon} renderBelowTabBar={() => ( { store.getState().openPane({ pane: tp("p1", "opened"), - tabTitle: "My Tab", }); expect(store.getState().tabs).toHaveLength(1); - expect(store.getState().tabs[0]?.titleOverride).toBe("My Tab"); expect(store.getState().getActivePane()?.pane.data.label).toBe("opened"); }); diff --git a/packages/panes/src/core/store/store.ts b/packages/panes/src/core/store/store.ts index 06442cf2a33..34778b9cb25 100644 --- a/packages/panes/src/core/store/store.ts +++ b/packages/panes/src/core/store/store.ts @@ -121,7 +121,7 @@ export interface WorkspaceStore extends WorkspaceState { newPane: CreatePaneInput; }) => void; - openPane: (args: { pane: CreatePaneInput; tabTitle?: string }) => void; + openPane: (args: { pane: CreatePaneInput }) => void; splitPane: (args: { tabId: string; @@ -406,7 +406,6 @@ export function createWorkspaceStore( // No tab → create one if (!tab || !activeTabId) { get().addTab({ - titleOverride: args.tabTitle, panes: [args.pane], }); return; diff --git a/packages/panes/src/index.ts b/packages/panes/src/index.ts index 4e08a915239..a4070aa2dcf 100644 --- a/packages/panes/src/index.ts +++ b/packages/panes/src/index.ts @@ -17,7 +17,7 @@ export type { TabContext, WorkspaceProps, } from "./react"; -export { Workspace } from "./react"; +export { resolveTabTitle, Workspace } from "./react"; export type { LayoutNode, Pane, diff --git a/packages/panes/src/react/components/Workspace/Workspace.tsx b/packages/panes/src/react/components/Workspace/Workspace.tsx index 34847171540..f0d3a72dd87 100644 --- a/packages/panes/src/react/components/Workspace/Workspace.tsx +++ b/packages/panes/src/react/components/Workspace/Workspace.tsx @@ -5,6 +5,7 @@ import type { Pane } from "../../../types"; import type { WorkspaceProps } from "../../types"; import { Tab } from "./components/Tab"; import { TabBar } from "./components/TabBar"; +import { resolveTabTitle } from "./utils/resolveTabTitle"; export function Workspace({ store, @@ -12,7 +13,6 @@ export function Workspace({ className, renderTabAccessory, renderTabIcon, - getTabTitle, renderEmptyState, renderAddTabMenu, renderBelowTabBar, @@ -78,7 +78,7 @@ export function Workspace({ onReorderTab={(tabId, toIndex) => store.getState().reorderTab({ tabId, toIndex }) } - getTabTitle={(tab) => tab.titleOverride ?? getTabTitle?.(tab) ?? tab.id} + getTabTitle={(tab) => resolveTabTitle(tab, tabs, registry)} renderTabIcon={renderTabIcon} renderAddTabMenu={renderAddTabMenu} renderTabAccessory={renderTabAccessory} diff --git a/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx b/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx index 1ac744d2c51..22096e349fe 100644 --- a/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx +++ b/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx @@ -211,7 +211,7 @@ export function Pane({ } const title = definition - ? (pane.titleOverride ?? definition.getTitle?.(context) ?? pane.id) + ? (pane.titleOverride ?? definition.getTitle?.(pane) ?? pane.id) : `Unknown: ${pane.kind}`; const icon = definition?.getIcon?.(context); const titleContent = definition?.renderTitle?.(context); diff --git a/packages/panes/src/react/components/Workspace/index.ts b/packages/panes/src/react/components/Workspace/index.ts index e904b737efa..ff44d323596 100644 --- a/packages/panes/src/react/components/Workspace/index.ts +++ b/packages/panes/src/react/components/Workspace/index.ts @@ -1 +1,2 @@ +export { resolveTabTitle } from "./utils/resolveTabTitle"; export { Workspace } from "./Workspace"; diff --git a/packages/panes/src/react/components/Workspace/utils/resolveTabTitle.ts b/packages/panes/src/react/components/Workspace/utils/resolveTabTitle.ts new file mode 100644 index 00000000000..388466a791a --- /dev/null +++ b/packages/panes/src/react/components/Workspace/utils/resolveTabTitle.ts @@ -0,0 +1,18 @@ +import type { Tab } from "../../../../types"; +import type { PaneRegistry } from "../../../types"; + +export function resolveTabTitle( + tab: Tab, + tabs: Tab[], + registry: PaneRegistry, +): string { + if (tab.titleOverride) return tab.titleOverride; + const panes = Object.values(tab.panes); + const onlyPane = panes.length === 1 ? panes[0] : undefined; + if (onlyPane) { + const fromPane = + onlyPane.titleOverride ?? registry[onlyPane.kind]?.getTitle?.(onlyPane); + if (fromPane) return fromPane; + } + return `Tab ${tabs.indexOf(tab) + 1}`; +} diff --git a/packages/panes/src/react/index.ts b/packages/panes/src/react/index.ts index c54e0d47c8f..4d825812559 100644 --- a/packages/panes/src/react/index.ts +++ b/packages/panes/src/react/index.ts @@ -1,4 +1,4 @@ -export { Workspace } from "./components/Workspace"; +export { resolveTabTitle, Workspace } from "./components/Workspace"; export type { ContextMenuActionConfig, PaneActionConfig, diff --git a/packages/panes/src/react/types.ts b/packages/panes/src/react/types.ts index d21c1f8d0f2..ab4b3058de0 100644 --- a/packages/panes/src/react/types.ts +++ b/packages/panes/src/react/types.ts @@ -58,7 +58,7 @@ export interface RendererContext { export interface PaneDefinition { renderPane(context: RendererContext): ReactNode; - getTitle?(context: RendererContext): ReactNode; + getTitle?(pane: Pane): string | undefined; getIcon?(context: RendererContext): ReactNode; renderTitle?(context: RendererContext): ReactNode; renderHeaderExtras?(context: RendererContext): ReactNode; @@ -88,7 +88,6 @@ export interface WorkspaceProps { className?: string; renderTabAccessory?: (tab: Tab) => ReactNode; renderTabIcon?: (tab: Tab) => ReactNode; - getTabTitle?: (tab: Tab) => string | undefined; renderEmptyState?: () => ReactNode; renderAddTabMenu?: () => ReactNode; renderBelowTabBar?: () => ReactNode;