From d930ff5b802a76ab02465e30f12febaf3982c73c Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Fri, 17 Apr 2026 23:36:20 -0700 Subject: [PATCH 1/3] fix(desktop): refresh v2 terminal link tooltip editor label + nudge plain clicks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LinkHoverTooltip fetched the default editor in a mount-only useEffect, so changing the default editor in settings left the modifier-shift label ("Open in Cursor", etc.) stale until the terminal pane unmounted. Refetch on every hover-start instead. - Plain (no-modifier) clicks on a detected file path in the v2 terminal were silent, which made the modifier-key affordance undiscoverable. On a plain file-link click, show a transient tooltip at the click position ("Hold ⌘ to open · ⌘⇧ for external", or Ctrl/Ctrl+Shift off-mac). Capped at two shows per renderer session via a module-level counter, and suppressed while the modifier-hover tooltip is already visible. Uses framer-motion's AnimatePresence for fade in/out. --- .../components/TerminalPane/TerminalPane.tsx | 10 ++- .../LinkHoverTooltip/LinkHoverTooltip.tsx | 62 ++++++++++++++----- .../hooks/useLinkClickHint/index.ts | 1 + .../useLinkClickHint/useLinkClickHint.ts | 35 +++++++++++ 4 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useLinkClickHint/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/hooks/useLinkClickHint/useLinkClickHint.ts 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..b8ca0436b46 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,15 +1,27 @@ import type { ExternalApp } from "@superset/local-db"; +import { AnimatePresence, motion } from "framer-motion"; import { useEffect, useState } from "react"; 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 { @@ -33,10 +45,12 @@ function getLabel( return info.isDirectory ? "Reveal in sidebar" : "Open in editor"; } -export function LinkHoverTooltip({ hoveredLink }: LinkHoverTooltipProps) { +export function LinkHoverTooltip({ hoveredLink, hint }: LinkHoverTooltipProps) { const [defaultEditor, setDefaultEditor] = useState(null); + const hovering = hoveredLink !== null; useEffect(() => { + if (!hovering) return; let cancelled = false; electronTrpcClient.settings.getDefaultEditor .query() @@ -54,22 +68,42 @@ export function LinkHoverTooltip({ hoveredLink }: LinkHoverTooltipProps) { return () => { cancelled = true; }; - }, []); - - if (!hoveredLink || !hoveredLink.modifier) return null; + }, [hovering]); - const label = getLabel(hoveredLink.info, hoveredLink.shift, defaultEditor); + const showingHover = Boolean(hoveredLink?.modifier); return createPortal( -
- {label} -
, + <> + {hoveredLink?.modifier && ( +
+ {getLabel(hoveredLink.info, hoveredLink.shift, defaultEditor)} +
+ )} + + {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 }; +} From 48254ebdfa968cbd3ca535e1e901f9fe2aefb2fa Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Fri, 17 Apr 2026 23:41:57 -0700 Subject: [PATCH 2/3] refactor(desktop): simpler v2 terminal link tooltip labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Open in editor" → "Open in pane" for the ⌘-click file case (native in-app file pane is what actually happens). - Shift variant always says "Open in external editor" instead of interpolating the configured editor name. openFileInEditor uses the global settings defaultEditor (non-editor apps like Finder can't be set there), so the interpolated name could disagree with a user's per-project preference — the generic label never lies. - Drops the getDefaultEditor fetch, defaultEditor state, and getAppOption/ getAppLabel plumbing that went with it. --- .../LinkHoverTooltip/LinkHoverTooltip.tsx | 49 ++----------------- 1 file changed, 4 insertions(+), 45 deletions(-) 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 b8ca0436b46..f81e1e47ece 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,10 +1,6 @@ -import type { ExternalApp } from "@superset/local-db"; import { AnimatePresence, motion } from "framer-motion"; -import { useEffect, useState } from "react"; 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"; @@ -24,52 +20,15 @@ interface LinkHoverTooltipProps { 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 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, hint }: LinkHoverTooltipProps) { - const [defaultEditor, setDefaultEditor] = useState(null); - const hovering = hoveredLink !== null; - - useEffect(() => { - if (!hovering) return; - 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; - }; - }, [hovering]); - const showingHover = Boolean(hoveredLink?.modifier); return createPortal( @@ -82,7 +41,7 @@ export function LinkHoverTooltip({ hoveredLink, hint }: LinkHoverTooltipProps) { top: hoveredLink.clientY + TOOLTIP_OFFSET_PX, }} > - {getLabel(hoveredLink.info, hoveredLink.shift, defaultEditor)} + {getLabel(hoveredLink.info, hoveredLink.shift)} )} From 1196764ef919f5fcdf4cf0df9d39f69c7cdbe545 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Fri, 17 Apr 2026 23:47:03 -0700 Subject: [PATCH 3/3] =?UTF-8?q?refactor(desktop):=20URL=20=E2=8C=98-click?= =?UTF-8?q?=20tooltip=20says=20"Open=20in=20pane"=20for=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/LinkHoverTooltip/LinkHoverTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f81e1e47ece..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 @@ -22,7 +22,7 @@ interface LinkHoverTooltipProps { function getLabel(info: LinkHoverInfo, shift: boolean): string { if (info.kind === "url") { - return shift ? "Open in external browser" : "Open in browser"; + return shift ? "Open in external browser" : "Open in pane"; } if (shift) return "Open in external editor"; return info.isDirectory ? "Reveal in sidebar" : "Open in pane";