diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabContentContextMenu.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabContentContextMenu.tsx index be086541967..92b8a4956fb 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabContentContextMenu.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabContentContextMenu.tsx @@ -3,12 +3,13 @@ import { ContextMenuContent, ContextMenuItem, ContextMenuSeparator, + ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@superset/ui/context-menu"; -import { Columns2, MoveRight, Plus, Rows2, X } from "lucide-react"; +import { Columns2, Eraser, MoveRight, Plus, Rows2, X } from "lucide-react"; import type { ReactNode } from "react"; import type { Tab } from "renderer/stores/tabs/types"; @@ -17,6 +18,7 @@ interface TabContentContextMenuProps { onSplitHorizontal: () => void; onSplitVertical: () => void; onClosePane: () => void; + onClearTerminal: () => void; currentTabId: string; availableTabs: Tab[]; onMoveToTab: (tabId: string) => void; @@ -28,6 +30,7 @@ export function TabContentContextMenu({ onSplitHorizontal, onSplitVertical, onClosePane, + onClearTerminal, currentTabId, availableTabs, onMoveToTab, @@ -48,6 +51,11 @@ export function TabContentContextMenu({ Split Vertically + + + Clear Terminal + ⌘K + @@ -73,7 +81,7 @@ export function TabContentContextMenu({ - Close Pane + Close Terminal diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx index 4b9d15705b0..792dc7110cd 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx @@ -7,6 +7,7 @@ import { registerPaneRef, unregisterPaneRef, } from "renderer/stores/tabs/pane-refs"; +import { useTerminalCallbacksStore } from "renderer/stores/tabs/terminal-callbacks"; import type { Pane, Tab } from "renderer/stores/tabs/types"; import { TabContentContextMenu } from "../TabContentContextMenu"; import { Terminal } from "../Terminal"; @@ -110,6 +111,11 @@ export function TabPane({ splitPaneAuto(tabId, paneId, { width, height }, path); }; + const getClearCallback = useTerminalCallbacksStore((s) => s.getClearCallback); + const handleClearTerminal = () => { + getClearCallback(paneId)?.(); + }; + const splitIcon = splitOrientation === "vertical" ? ( @@ -147,6 +153,7 @@ export function TabPane({ onSplitHorizontal={() => splitPaneHorizontal(tabId, paneId, path)} onSplitVertical={() => splitPaneVertical(tabId, paneId, path)} onClosePane={() => removePane(paneId)} + onClearTerminal={handleClearTerminal} currentTabId={tabId} availableTabs={availableTabs} onMoveToTab={onMoveToTab} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx index 04ee96146ea..3e55aada271 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx @@ -7,6 +7,7 @@ import { useEffect, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { trpc } from "renderer/lib/trpc"; import { useTabsStore } from "renderer/stores/tabs/store"; +import { useTerminalCallbacksStore } from "renderer/stores/tabs/terminal-callbacks"; import { useTerminalTheme } from "renderer/stores/theme"; import { HOTKEYS } from "shared/hotkeys"; import { sanitizeForTitle } from "./commandBuffer"; @@ -80,6 +81,13 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { detachRef.current = detachMutation.mutate; clearScrollbackRef.current = clearScrollbackMutation.mutate; + const registerClearCallbackRef = useRef( + useTerminalCallbacksStore.getState().registerClearCallback, + ); + const unregisterClearCallbackRef = useRef( + useTerminalCallbacksStore.getState().unregisterClearCallback, + ); + const parentTabIdRef = useRef(parentTabId); parentTabIdRef.current = parentTabId; @@ -293,18 +301,23 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { const inputDisposable = xterm.onData(handleTerminalInput); const keyDisposable = xterm.onKey(handleKeyPress); + const handleClear = () => { + xterm.clear(); + clearScrollbackRef.current({ paneId }); + }; + const cleanupKeyboard = setupKeyboardHandler(xterm, { onShiftEnter: () => { if (!isExitedRef.current) { writeRef.current({ paneId, data: "\\\n" }); } }, - onClear: () => { - xterm.clear(); - clearScrollbackRef.current({ paneId }); - }, + onClear: handleClear, }); + // Register clear callback for context menu access + registerClearCallbackRef.current(paneId, handleClear); + const cleanupFocus = setupFocusListener(xterm, () => handleTerminalFocusRef.current(), ); @@ -331,6 +344,7 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { cleanupResize(); cleanupPaste(); cleanupQuerySuppression(); + unregisterClearCallbackRef.current(paneId); debouncedSetTabAutoTitleRef.current?.cancel?.(); // Detach instead of kill to keep PTY running for reattachment detachRef.current({ paneId }); diff --git a/apps/desktop/src/renderer/stores/tabs/terminal-callbacks.ts b/apps/desktop/src/renderer/stores/tabs/terminal-callbacks.ts new file mode 100644 index 00000000000..eb69d7cb78e --- /dev/null +++ b/apps/desktop/src/renderer/stores/tabs/terminal-callbacks.ts @@ -0,0 +1,34 @@ +import { create } from "zustand"; + +interface TerminalCallbacksState { + clearCallbacks: Map void>; + registerClearCallback: (paneId: string, callback: () => void) => void; + unregisterClearCallback: (paneId: string) => void; + getClearCallback: (paneId: string) => (() => void) | undefined; +} + +export const useTerminalCallbacksStore = create()( + (set, get) => ({ + clearCallbacks: new Map(), + + registerClearCallback: (paneId, callback) => { + set((state) => { + const newCallbacks = new Map(state.clearCallbacks); + newCallbacks.set(paneId, callback); + return { clearCallbacks: newCallbacks }; + }); + }, + + unregisterClearCallback: (paneId) => { + set((state) => { + const newCallbacks = new Map(state.clearCallbacks); + newCallbacks.delete(paneId); + return { clearCallbacks: newCallbacks }; + }); + }, + + getClearCallback: (paneId) => { + return get().clearCallbacks.get(paneId); + }, + }), +);