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 8eb1d762401..7f7eaec3c65 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 @@ -26,6 +26,7 @@ import { TerminalSearch } from "renderer/screens/main/components/WorkspaceView/C import { useTheme } from "renderer/stores/theme"; import { resolveTerminalThemeType } from "renderer/stores/theme/utils"; import { LinkHoverTooltip } from "./components/LinkHoverTooltip"; +import { useLinkClickHint } from "./hooks/useLinkClickHint"; import { useLinkHoverState } from "./hooks/useLinkHoverState"; import { useTerminalAppearance } from "./hooks/useTerminalAppearance"; import { shellEscapePaths } from "./utils"; @@ -58,6 +59,7 @@ export function TerminalPane({ onHover: onLinkHover, onLeave: onLinkLeave, } = useLinkHoverState(); + const { hint, showHint } = useLinkClickHint(); const paneData = ctx.pane.data as TerminalPaneData; const { terminalId } = paneData; const containerRef = useRef(null); @@ -159,7 +161,10 @@ export function TerminalPane({ } }, onFileLinkClick: (event, link) => { - if (!event.metaKey && !event.ctrlKey) return; + if (!event.metaKey && !event.ctrlKey) { + showHint(event.clientX, event.clientY); + return; + } event.preventDefault(); if (event.shiftKey) { openInExternalEditor(link.resolvedPath, { @@ -200,6 +205,7 @@ export function TerminalPane({ openInExternalEditor, onLinkHover, onLinkLeave, + showHint, ]); useHotkey( @@ -312,7 +318,7 @@ export function TerminalPane({ Disconnected )} - + ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/LinkHoverTooltip/LinkHoverTooltip.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/LinkHoverTooltip/LinkHoverTooltip.tsx index 87323399029..36cc26e390d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/LinkHoverTooltip/LinkHoverTooltip.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/LinkHoverTooltip/LinkHoverTooltip.tsx @@ -1,75 +1,68 @@ -import type { ExternalApp } from "@superset/local-db"; -import { useEffect, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; import { createPortal } from "react-dom"; -import { getAppOption } from "renderer/components/OpenInExternalDropdown/constants"; import type { LinkHoverInfo } from "renderer/lib/terminal/terminal-runtime-registry"; -import { electronTrpcClient } from "renderer/lib/trpc-client"; +import type { LinkClickHint } from "../../hooks/useLinkClickHint"; import type { HoveredLink } from "../../hooks/useLinkHoverState"; const TOOLTIP_OFFSET_PX = 14; +const TOOLTIP_CLASSES = + "pointer-events-none fixed z-50 w-fit rounded-md bg-foreground px-3 py-1.5 text-xs text-background"; + +const isMac = + typeof navigator !== "undefined" && + navigator.platform.toLowerCase().includes("mac"); +const MOD_LABEL = isMac ? "⌘" : "Ctrl"; +const MOD_SHIFT_LABEL = isMac ? "⌘⇧" : "Ctrl+Shift"; +const HINT_LABEL = `Hold ${MOD_LABEL} to open · ${MOD_SHIFT_LABEL} for external`; interface LinkHoverTooltipProps { hoveredLink: HoveredLink | null; + hint: LinkClickHint | null; } -function getAppLabel(app: ExternalApp): string { - const option = getAppOption(app); - return option?.displayLabel ?? option?.label ?? "external editor"; -} - -function getLabel( - info: LinkHoverInfo, - shift: boolean, - defaultEditor: ExternalApp | null, -): string { +function getLabel(info: LinkHoverInfo, shift: boolean): string { if (info.kind === "url") { - return shift ? "Open in external browser" : "Open in browser"; - } - if (shift) { - return defaultEditor - ? `Open in ${getAppLabel(defaultEditor)}` - : "Open externally"; + return shift ? "Open in external browser" : "Open in pane"; } - return info.isDirectory ? "Reveal in sidebar" : "Open in editor"; + if (shift) return "Open in external editor"; + return info.isDirectory ? "Reveal in sidebar" : "Open in pane"; } -export function LinkHoverTooltip({ hoveredLink }: LinkHoverTooltipProps) { - const [defaultEditor, setDefaultEditor] = useState(null); - - useEffect(() => { - let cancelled = false; - electronTrpcClient.settings.getDefaultEditor - .query() - .then((editor) => { - if (!cancelled) setDefaultEditor(editor); - }) - .catch((error) => { - if (cancelled) return; - console.warn( - "[LinkHoverTooltip] Failed to fetch default editor:", - error, - ); - setDefaultEditor(null); - }); - return () => { - cancelled = true; - }; - }, []); - - if (!hoveredLink || !hoveredLink.modifier) return null; - - const label = getLabel(hoveredLink.info, hoveredLink.shift, defaultEditor); +export function LinkHoverTooltip({ hoveredLink, hint }: LinkHoverTooltipProps) { + const showingHover = Boolean(hoveredLink?.modifier); return createPortal( -
- {label} -
, + <> + {hoveredLink?.modifier && ( +
+ {getLabel(hoveredLink.info, hoveredLink.shift)} +
+ )} + + {hint && !showingHover && ( + + {HINT_LABEL} + + )} + + , document.body, ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useLinkClickHint/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useLinkClickHint/index.ts new file mode 100644 index 00000000000..c9aacc8b15e --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useLinkClickHint/index.ts @@ -0,0 +1 @@ +export { type LinkClickHint, useLinkClickHint } from "./useLinkClickHint"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useLinkClickHint/useLinkClickHint.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useLinkClickHint/useLinkClickHint.ts new file mode 100644 index 00000000000..4c6d5286f58 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useLinkClickHint/useLinkClickHint.ts @@ -0,0 +1,35 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +export interface LinkClickHint { + clientX: number; + clientY: number; +} + +const HINT_DURATION_MS = 2000; +const MAX_HINTS_PER_SESSION = 2; + +let hintsRemaining = MAX_HINTS_PER_SESSION; + +export function useLinkClickHint() { + const [hint, setHint] = useState(null); + const timeoutRef = useRef | null>(null); + + const showHint = useCallback((clientX: number, clientY: number) => { + if (hintsRemaining <= 0) return; + hintsRemaining -= 1; + if (timeoutRef.current) clearTimeout(timeoutRef.current); + setHint({ clientX, clientY }); + timeoutRef.current = setTimeout(() => { + setHint(null); + timeoutRef.current = null; + }, HINT_DURATION_MS); + }, []); + + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); + + return { hint, showHint }; +}