diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/V2PresetsBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/V2PresetsBar.tsx index 4f6a763070c..716e9c77759 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/V2PresetsBar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/V2PresetsBar.tsx @@ -24,7 +24,7 @@ import { V2PresetBarItem } from "./components/V2PresetBarItem"; interface V2PresetsBarProps { matchedPresets: V2TerminalPresetRow[]; - executePreset: (preset: V2TerminalPresetRow) => void; + executePreset: (preset: V2TerminalPresetRow) => void | Promise; } // Co-located to keep v2 self-contained. Mirrors the v1 array in 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 43804012141..4d81645da6d 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 @@ -22,10 +22,15 @@ import type { PaneViewerData, TerminalPaneData, } from "../../types"; +import type { TerminalLauncher } from "../useV2TerminalLauncher"; -export function useDefaultContextMenuActions( - paneRegistry: PaneRegistry, -): ContextMenuActionConfig[] { +export function useDefaultContextMenuActions({ + paneRegistry, + launcher, +}: { + paneRegistry: PaneRegistry; + launcher: TerminalLauncher; +}): ContextMenuActionConfig[] { const splitDownShortcut = useHotkeyDisplay("SPLIT_DOWN").text; const splitRightShortcut = useHotkeyDisplay("SPLIT_RIGHT").text; const splitWithChatShortcut = useHotkeyDisplay("SPLIT_WITH_CHAT").text; @@ -43,12 +48,11 @@ export function useDefaultContextMenuActions( icon: , shortcut: splitDownShortcut !== "Unassigned" ? splitDownShortcut : undefined, - onSelect: (ctx) => { + onSelect: async (ctx) => { + const terminalId = await launcher.create(); ctx.actions.split("down", { kind: "terminal", - data: { - terminalId: crypto.randomUUID(), - } as TerminalPaneData, + data: { terminalId } as TerminalPaneData, }); }, }, @@ -58,12 +62,11 @@ export function useDefaultContextMenuActions( icon: , shortcut: splitRightShortcut !== "Unassigned" ? splitRightShortcut : undefined, - onSelect: (ctx) => { + onSelect: async (ctx) => { + const terminalId = await launcher.create(); ctx.actions.split("right", { kind: "terminal", - data: { - terminalId: crypto.randomUUID(), - } as TerminalPaneData, + data: { terminalId } as TerminalPaneData, }); }, }, @@ -162,6 +165,7 @@ export function useDefaultContextMenuActions( equalizePaneSplitsShortcut, closePaneShortcut, paneRegistry, + launcher, ], ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultPaneActions/useDefaultPaneActions.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultPaneActions/useDefaultPaneActions.tsx index 5699b313ca8..1bcf2164b4e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultPaneActions/useDefaultPaneActions.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultPaneActions/useDefaultPaneActions.tsx @@ -4,8 +4,13 @@ import { HiMiniXMark } from "react-icons/hi2"; import { TbLayoutColumns, TbLayoutRows } from "react-icons/tb"; import { HotkeyLabel } from "renderer/hotkeys"; import type { PaneViewerData, TerminalPaneData } from "../../types"; +import type { TerminalLauncher } from "../useV2TerminalLauncher"; -export function useDefaultPaneActions(): PaneActionConfig[] { +export function useDefaultPaneActions({ + launcher, +}: { + launcher: TerminalLauncher; +}): PaneActionConfig[] { return useMemo[]>( () => [ { @@ -17,14 +22,13 @@ export function useDefaultPaneActions(): PaneActionConfig[] { ), tooltip: , - onClick: (ctx) => { + onClick: async (ctx) => { const position = ctx.pane.parentDirection === "horizontal" ? "down" : "right"; + const terminalId = await launcher.create(); ctx.actions.split(position, { kind: "terminal", - data: { - terminalId: crypto.randomUUID(), - } as TerminalPaneData, + data: { terminalId } as TerminalPaneData, }); }, }, @@ -35,6 +39,6 @@ export function useDefaultPaneActions(): PaneActionConfig[] { onClick: (ctx) => ctx.actions.close(), }, ], - [], + [launcher], ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx index 0f9bb6b724d..f5b6d3d6133 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx @@ -1,6 +1,6 @@ import type { RendererContext } from "@superset/panes"; import { cn } from "@superset/ui/utils"; -import { useWorkspaceClient, workspaceTrpc } from "@superset/workspace-client"; +import { workspaceTrpc } from "@superset/workspace-client"; import "@xterm/xterm/css/xterm.css"; import { useCallback, @@ -33,8 +33,6 @@ import { openUrlInV2Workspace } from "renderer/routes/_authenticated/_dashboard/ import { useWorkspaceWsUrl } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceTrpcProvider/WorkspaceTrpcProvider"; import { ScrollToBottomButton } from "renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/ScrollToBottomButton"; import { TerminalSearch } from "renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/TerminalSearch"; -import { useTheme } from "renderer/stores/theme"; -import { resolveTerminalThemeType } from "renderer/stores/theme/utils"; import { useLinkClickHint } from "./hooks/useLinkClickHint"; import { type HoveredLink, useLinkHoverState } from "./hooks/useLinkHoverState"; import { useTerminalAppearance } from "./hooks/useTerminalAppearance"; @@ -64,45 +62,20 @@ export function TerminalPane({ const { hint, showHint } = useLinkClickHint(); const openInExternalEditor = useOpenInExternalEditor(workspaceId); const paneData = ctx.pane.data as TerminalPaneData; - const paneDataRef = useRef(paneData); - paneDataRef.current = paneData; - const paneActionsRef = useRef(ctx.actions); - paneActionsRef.current = ctx.actions; const { terminalId } = paneData; - const initialCommandRef = useRef(paneData.initialCommand); const terminalInstanceId = ctx.pane.id; const containerRef = useRef(null); - const activeTheme = useTheme(); const [isSearchOpen, setIsSearchOpen] = useState(false); const appearance = useTerminalAppearance(); const appearanceRef = useRef(appearance); appearanceRef.current = appearance; - const initialThemeTypeRef = useRef< - ReturnType - >( - resolveTerminalThemeType({ - activeThemeType: activeTheme?.type, - }), - ); - const { trpcClient } = useWorkspaceClient(); - const trpcClientRef = useRef(trpcClient); - trpcClientRef.current = trpcClient; const websocketUrl = useWorkspaceWsUrl(`/terminal/${terminalId}`); const websocketUrlRef = useRef(websocketUrl); websocketUrlRef.current = websocketUrl; const workspaceIdRef = useRef(workspaceId); workspaceIdRef.current = workspaceId; - const markInitialCommandAccepted = useCallback(() => { - initialCommandRef.current = undefined; - const currentPaneData = paneDataRef.current; - if (currentPaneData.initialCommand === undefined) return; - paneActionsRef.current.updateData({ - ...currentPaneData, - initialCommand: undefined, - } as PaneViewerData); - }, []); const workspaceTrpcUtils = workspaceTrpc.useUtils(); const invalidateTerminalSessionsRef = useRef( @@ -139,17 +112,17 @@ export function TerminalPane({ // is visible immediately, even on cold start. For a warm return // (workspace switch) this reparents the wrapper from the parking // container back into the live tree, preserving the buffer. - // 2. createSession() starts or adopts the server terminal using the - // measured dimensions and optional initial command. - // 3. connect() attaches the WebSocket to that terminalId. The socket is + // 2. connect() attaches the WebSocket to that terminalId. The socket is // transport only; it does not carry creation-time intent. + // The pane never calls createSession — that's useV2TerminalLauncher's job, + // awaited at the call site before the pane is added to the store. By the + // time this effect runs, the host-service session already exists. // Deps narrowed to the terminal identity so provider key remount churn // (workspaceId/client briefly flipping while pane data catches up) doesn't // re-run this effect. Mutable inputs are read through refs. useEffect(() => { const container = containerRef.current; if (!container) return; - let cancelled = false; terminalRuntimeRegistry.mount( terminalId, @@ -158,51 +131,16 @@ export function TerminalPane({ terminalInstanceId, ); - const dimensions = terminalRuntimeRegistry.getDimensions( + terminalRuntimeRegistry.connect( terminalId, + websocketUrlRef.current, terminalInstanceId, ); - const pendingInitialCommand = initialCommandRef.current?.trim() - ? initialCommandRef.current - : undefined; - - void (async () => { - try { - await trpcClientRef.current.terminal.createSession.mutate({ - terminalId, - workspaceId: workspaceIdRef.current, - themeType: initialThemeTypeRef.current, - initialCommand: pendingInitialCommand, - cols: dimensions?.cols, - rows: dimensions?.rows, - }); - if (cancelled) return; - if (pendingInitialCommand) markInitialCommandAccepted(); - } catch (error) { - if (cancelled) return; - const message = - error instanceof Error - ? error.message - : "Failed to create terminal session"; - terminalRuntimeRegistry - .getTerminal(terminalId, terminalInstanceId) - ?.writeln(`\r\n[terminal] ${message}`); - return; - } - - if (cancelled) return; - terminalRuntimeRegistry.connect( - terminalId, - websocketUrlRef.current, - terminalInstanceId, - ); - })(); return () => { - cancelled = true; terminalRuntimeRegistry.detach(terminalId, terminalInstanceId); }; - }, [terminalId, terminalInstanceId, markInitialCommandAccepted]); + }, [terminalId, terminalInstanceId]); const lastInvalidatedOpenSessionRef = useRef(null); useEffect(() => { 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 5e6acb75c6a..75e1096f471 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 @@ -11,16 +11,16 @@ import { getPresetLaunchPlan } from "renderer/stores/tabs/preset-launch"; import { filterMatchingPresetsForProject } from "shared/preset-project-targeting"; import type { StoreApi } from "zustand/vanilla"; import type { PaneViewerData, TerminalPaneData } from "../../types"; +import type { TerminalLauncher } from "../useV2TerminalLauncher"; function makeTerminalPane( terminalId: string, titleOverride?: string, - initialCommand?: string, ): CreatePaneInput { return { kind: "terminal", titleOverride, - data: { terminalId, initialCommand } as TerminalPaneData, + data: { terminalId } as TerminalPaneData, }; } @@ -30,9 +30,13 @@ function resolveTarget(executionMode: V2TerminalPresetRow["executionMode"]) { interface UseV2PresetExecutionArgs { store: StoreApi>; + launcher: TerminalLauncher; } -export function useV2PresetExecution({ store }: UseV2PresetExecutionArgs) { +export function useV2PresetExecution({ + store, + launcher, +}: UseV2PresetExecutionArgs) { const { workspace } = useWorkspace(); const projectId = workspace.projectId; const collections = useCollections(); @@ -79,10 +83,11 @@ export function useV2PresetExecution({ store }: UseV2PresetExecutionArgs) { ); const executePreset = useCallback( - (preset: V2TerminalPresetRow) => { + async (preset: V2TerminalPresetRow) => { const state = store.getState(); const activeTabId = state.activeTabId; const target = resolveTarget(preset.executionMode); + const title = preset.name || undefined; const commands = resolvePresetCommands(preset); const plan = getPresetLaunchPlan({ @@ -92,108 +97,74 @@ export function useV2PresetExecution({ store }: UseV2PresetExecutionArgs) { hasActiveTab: !!activeTabId, }); + // Sessions for every pane this plan creates are spun up in parallel + // before any of them land in the store, so background tabs (e.g. + // new-tab-per-command, where each addTab flips activeTabId and only + // the last tab ever mounts) still get their PTY + initial command — + // host-service buffers PTY output until the user clicks the tab and + // the pane finally mounts and attaches the WS. try { switch (plan) { case "new-tab-single": { - const id = crypto.randomUUID(); - state.addTab({ - panes: [ - makeTerminalPane(id, preset.name || undefined, commands[0]), - ], - }); + const terminalId = await launcher.create({ command: commands[0] }); + state.addTab({ panes: [makeTerminalPane(terminalId, title)] }); break; } case "new-tab-multi-pane": { - const panes = commands.map((command) => - makeTerminalPane( - crypto.randomUUID(), - preset.name || undefined, - command, - ), + const ids = await Promise.all( + commands.length > 0 + ? commands.map((command) => launcher.create({ command })) + : [launcher.create()], ); state.addTab({ - panes: - panes.length > 0 - ? (panes as [ - CreatePaneInput, - ...CreatePaneInput[], - ]) - : [ - makeTerminalPane( - crypto.randomUUID(), - preset.name || undefined, - ), - ], + panes: ids.map((id) => makeTerminalPane(id, title)) as [ + CreatePaneInput, + ...CreatePaneInput[], + ], }); break; } case "new-tab-per-command": { - for (const command of commands) { - state.addTab({ - panes: [ - makeTerminalPane( - crypto.randomUUID(), - preset.name || undefined, - command, - ), - ], - }); + const ids = await Promise.all( + commands.map((command) => launcher.create({ command })), + ); + for (const terminalId of ids) { + state.addTab({ panes: [makeTerminalPane(terminalId, title)] }); } break; } case "active-tab-single": { - const id = crypto.randomUUID(); - const pane = makeTerminalPane( - id, - preset.name || undefined, - commands[0], - ); + const terminalId = await launcher.create({ command: commands[0] }); + const pane = makeTerminalPane(terminalId, title); if (!activeTabId) { - state.addTab({ - panes: [pane], - }); + state.addTab({ panes: [pane] }); break; } - state.addPane({ - tabId: activeTabId, - pane, - }); + state.addPane({ tabId: activeTabId, pane }); break; } case "active-tab-multi-pane": { - const panes = commands.map((command) => - makeTerminalPane( - crypto.randomUUID(), - preset.name || undefined, - command, - ), + const ids = await Promise.all( + commands.length > 0 + ? commands.map((command) => launcher.create({ command })) + : [launcher.create()], ); + const panes = ids.map((id) => makeTerminalPane(id, title)); if (!activeTabId) { state.addTab({ - panes: - panes.length > 0 - ? (panes as [ - CreatePaneInput, - ...CreatePaneInput[], - ]) - : [ - makeTerminalPane( - crypto.randomUUID(), - preset.name || undefined, - ), - ], + panes: panes as [ + CreatePaneInput, + ...CreatePaneInput[], + ], }); break; } for (const pane of panes) { - state.addPane({ - tabId: activeTabId, - pane, - }); + state.addPane({ tabId: activeTabId, pane }); } break; } @@ -208,7 +179,7 @@ export function useV2PresetExecution({ store }: UseV2PresetExecutionArgs) { }); } }, - [store, resolvePresetCommands], + [store, launcher, resolvePresetCommands], ); return { matchedPresets, executePreset }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/index.ts new file mode 100644 index 00000000000..96188873f16 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/index.ts @@ -0,0 +1,4 @@ +export { + type TerminalLauncher, + useV2TerminalLauncher, +} from "./useV2TerminalLauncher"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts new file mode 100644 index 00000000000..ffaab2e70aa --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts @@ -0,0 +1,57 @@ +import { useWorkspaceClient } from "@superset/workspace-client"; +import { useCallback, useMemo } from "react"; +import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; +import { useTheme } from "renderer/stores/theme"; +import { resolveTerminalThemeType } from "renderer/stores/theme/utils"; + +interface CreateOptions { + /** + * If provided, the launcher uses this id instead of minting one. Use it + * when you already have a terminalId (e.g. rehydrating from a persisted + * pane layout). host-service createSession is idempotent: existing + * in-memory session → no-op; daemon PTY survived a host-service restart → + * adopt; nothing exists → spawn fresh. + */ + terminalId?: string; + command?: string; +} + +export interface TerminalLauncher { + /** + * Awaits `terminal.createSession` and returns the terminalId. Callers + * should await this before writing the pane into the store, so the pane's + * WebSocket connect doesn't race ahead of the session existing on + * host-service. + */ + create(options?: CreateOptions): Promise; +} + +export function useV2TerminalLauncher(): TerminalLauncher { + const { workspace } = useWorkspace(); + const { trpcClient } = useWorkspaceClient(); + const activeTheme = useTheme(); + const themeType = resolveTerminalThemeType({ + activeThemeType: activeTheme?.type, + }); + const workspaceId = workspace.id; + + const create = useCallback( + async (options?: CreateOptions): Promise => { + const terminalId = options?.terminalId ?? crypto.randomUUID(); + await trpcClient.terminal.createSession.mutate({ + terminalId, + workspaceId, + themeType, + initialCommand: options?.command, + }); + return terminalId; + }, + [trpcClient, workspaceId, themeType], + ); + + // Memoize so the launcher reference is stable across renders — every + // consumer lists `launcher` in a deps array (preset hook, hotkeys, pane + // actions, context menu, openers), and a fresh object literal each render + // would needlessly invalidate all those memos. + return useMemo(() => ({ create }), [create]); +} 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 67612fda052..9e0cf6cd125 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 @@ -17,17 +17,20 @@ import type { PaneViewerData, TerminalPaneData, } from "../../types"; +import type { TerminalLauncher } from "../useV2TerminalLauncher"; export function useWorkspaceHotkeys({ store, matchedPresets, executePreset, paneRegistry, + launcher, }: { store: StoreApi>; matchedPresets: V2TerminalPresetRow[]; - executePreset: (preset: V2TerminalPresetRow) => void; + executePreset: (preset: V2TerminalPresetRow) => void | Promise; paneRegistry: PaneRegistry; + launcher: TerminalLauncher; }) { const { setRightSidebarOpen, setRightSidebarTab } = useV2UserPreferences(); const visiblePresets = useMemo( @@ -41,12 +44,13 @@ export function useWorkspaceHotkeys({ // --- Tab creation --- - useHotkey("NEW_GROUP", () => { + useHotkey("NEW_GROUP", async () => { + const terminalId = await launcher.create(); store.getState().addTab({ panes: [ { kind: "terminal", - data: { terminalId: crypto.randomUUID() } as TerminalPaneData, + data: { terminalId } as TerminalPaneData, }, ], }); @@ -197,7 +201,7 @@ export function useWorkspaceHotkeys({ useHotkey("FOCUS_PANE_UP", () => moveFocusDirectional("up")); useHotkey("FOCUS_PANE_DOWN", () => moveFocusDirectional("down")); - useHotkey("SPLIT_AUTO", () => { + useHotkey("SPLIT_AUTO", async () => { const state = store.getState(); const active = state.getActivePane(); if (!active) return; @@ -206,43 +210,46 @@ export function useWorkspaceHotkeys({ ? getPaneParentDirection(tab.layout, active.pane.id) : null; const position = parentDirection === "horizontal" ? "bottom" : "right"; + const terminalId = await launcher.create(); state.splitPane({ tabId: active.tabId, paneId: active.pane.id, position, newPane: { kind: "terminal", - data: { terminalId: crypto.randomUUID() } as TerminalPaneData, + data: { terminalId } as TerminalPaneData, }, }); }); - useHotkey("SPLIT_RIGHT", () => { + useHotkey("SPLIT_RIGHT", async () => { const state = store.getState(); const active = state.getActivePane(); if (!active) return; + const terminalId = await launcher.create(); state.splitPane({ tabId: active.tabId, paneId: active.pane.id, position: "right", newPane: { kind: "terminal", - data: { terminalId: crypto.randomUUID() } as TerminalPaneData, + data: { terminalId } as TerminalPaneData, }, }); }); - useHotkey("SPLIT_DOWN", () => { + useHotkey("SPLIT_DOWN", async () => { const state = store.getState(); const active = state.getActivePane(); if (!active) return; + const terminalId = await launcher.create(); state.splitPane({ tabId: active.tabId, paneId: active.pane.id, position: "bottom", newPane: { kind: "terminal", - data: { terminalId: crypto.randomUUID() } as TerminalPaneData, + data: { terminalId } as TerminalPaneData, }, }); }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts index 9453e4e5fa6..e8847226d78 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts @@ -9,14 +9,17 @@ import type { PaneViewerData, TerminalPaneData, } from "../../types"; +import type { TerminalLauncher } from "../useV2TerminalLauncher"; export function useWorkspacePaneOpeners({ store, + launcher, }: { store: StoreApi>; + launcher: TerminalLauncher; }): { openDiffPane: (filePath: string, openInNewTab?: boolean) => void; - addTerminalTab: () => void; + addTerminalTab: () => Promise; addChatTab: () => void; addBrowserTab: () => void; openCommentPane: (comment: CommentPaneData) => void; @@ -76,18 +79,17 @@ export function useWorkspacePaneOpeners({ [store], ); - const addTerminalTab = useCallback(() => { + const addTerminalTab = useCallback(async () => { + const terminalId = await launcher.create(); store.getState().addTab({ panes: [ { kind: "terminal", - data: { - terminalId: crypto.randomUUID(), - } as TerminalPaneData, + data: { terminalId } as TerminalPaneData, }, ], }); - }, [store]); + }, [store, launcher]); const addChatTab = useCallback(() => { store.getState().addTab({ 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 931d4ecaae0..d39e9f031b2 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 @@ -23,6 +23,7 @@ import { useDirtyTabCloseGuard } from "./hooks/useDirtyTabCloseGuard"; import { usePaneRegistry } from "./hooks/usePaneRegistry"; import { renderBrowserTabIcon } from "./hooks/usePaneRegistry/components/BrowserPane"; import { useV2PresetExecution } from "./hooks/useV2PresetExecution"; +import { useV2TerminalLauncher } from "./hooks/useV2TerminalLauncher"; import { useV2WorkspacePaneLayout } from "./hooks/useV2WorkspacePaneLayout"; import { useWorkspaceFileNavigation } from "./hooks/useWorkspaceFileNavigation"; import { useWorkspaceHotkeys } from "./hooks/useWorkspaceHotkeys"; @@ -85,7 +86,11 @@ function V2WorkspacePage() { } = useV2UserPreferences(); const { store } = useV2WorkspacePaneLayout(); useClearActivePaneAttention({ store }); - const { matchedPresets, executePreset } = useV2PresetExecution({ store }); + const launcher = useV2TerminalLauncher(); + const { matchedPresets, executePreset } = useV2PresetExecution({ + store, + launcher, + }); useConsumeAutomationRunLink({ store, terminalId, @@ -116,18 +121,21 @@ function V2WorkspacePage() { onOpenFile: openFilePane, onRevealPath: revealPath, }); - const defaultContextMenuActions = useDefaultContextMenuActions(paneRegistry); + const defaultContextMenuActions = useDefaultContextMenuActions({ + paneRegistry, + launcher, + }); const { openDiffPane, addTerminalTab, addChatTab, addBrowserTab, openCommentPane, - } = useWorkspacePaneOpeners({ store }); + } = useWorkspacePaneOpeners({ store, launcher }); const [quickOpenOpen, setQuickOpenOpen] = useState(false); const handleQuickOpen = useCallback(() => setQuickOpenOpen(true), []); - const defaultPaneActions = useDefaultPaneActions(); + const defaultPaneActions = useDefaultPaneActions({ launcher }); const onBeforeCloseTab = useDirtyTabCloseGuard(); const sidebarOpen = v2UserPreferences.rightSidebarOpen; @@ -166,6 +174,7 @@ function V2WorkspacePage() { matchedPresets, executePreset, paneRegistry, + launcher, }); useHotkey("QUICK_OPEN", handleQuickOpen); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts index c5ef6e83376..82792c9543d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts @@ -8,7 +8,6 @@ export interface FilePaneData { export interface TerminalPaneData { terminalId: string; - initialCommand?: string; } export interface ChatPaneData {