Skip to content

Commit 35457b1

Browse files
committed
feat(desktop): modifier-keyed v2 terminal file links + folder sidebar reveal
Replaces the settings-based branch with a modifier-key pattern: - Cmd/Ctrl-click a file path → opens in an in-app FilePane. - Cmd/Ctrl+Shift-click a file or directory → opens in the external editor (with an upfront toast for remote workspaces, same pattern as FilesTab's Open in editor guard). - Cmd/Ctrl-click a directory path → force-opens the sidebar, reveals the folder in the file tree (ancestors expand, row scrolls into view and highlights). Implementation reuses the existing selectedFilePath → fileTree.reveal machinery in FilesTab by promoting selectedFilePath from a pane-store derivation to a useState, synced from the active file pane via useEffect. Folder focus is just a direct setSelectedFilePath — the existing sidebar code path handles reveal + scroll + highlight without changes. Folder paths now also flow through getParentForCreation, so the "New File" toolbar button creates inside the focused folder. Three callbacks (onOpenFile / onRevealPath / onOpenExternal) are plumbed from page.tsx through usePaneRegistry to TerminalPane. The shift-modifier path goes through openExternal, which checks workspaceHost.hostMachineId !== machineId and toasts for remote workspaces instead of firing a mutation the remote won't satisfy. v1 code untouched; DB schema untouched; v2 settings UI untouched (terminalLinkBehavior still honored by v1).
1 parent 93cc30b commit 35457b1

File tree

3 files changed

+122
-36
lines changed

3 files changed

+122
-36
lines changed

apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { RendererContext } from "@superset/panes";
2-
import { toast } from "@superset/ui/sonner";
32
import { workspaceTrpc } from "@superset/workspace-client";
43
import "@xterm/xterm/css/xterm.css";
54
import {
@@ -16,7 +15,6 @@ import {
1615
} from "renderer/lib/terminal/terminal-runtime-registry";
1716
import { electronTrpcClient } from "renderer/lib/trpc-client";
1817
import type {
19-
FilePaneData,
2018
PaneViewerData,
2119
TerminalPaneData,
2220
} from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types";
@@ -30,6 +28,12 @@ import { useTerminalAppearance } from "./hooks/useTerminalAppearance";
3028
interface TerminalPaneProps {
3129
ctx: RendererContext<PaneViewerData>;
3230
workspaceId: string;
31+
onOpenFile: (path: string, openInNewTab?: boolean) => void;
32+
onRevealPath: (path: string) => void;
33+
onOpenExternal: (
34+
path: string,
35+
opts?: { line?: number; column?: number },
36+
) => void;
3337
}
3438

3539
function subscribeToState(terminalId: string) {
@@ -41,7 +45,13 @@ function getConnectionState(terminalId: string): ConnectionState {
4145
return terminalRuntimeRegistry.getConnectionState(terminalId);
4246
}
4347

44-
export function TerminalPane({ ctx, workspaceId }: TerminalPaneProps) {
48+
export function TerminalPane({
49+
ctx,
50+
workspaceId,
51+
onOpenFile,
52+
onRevealPath,
53+
onOpenExternal,
54+
}: TerminalPaneProps) {
4555
const paneData = ctx.pane.data as TerminalPaneData;
4656
const { terminalId } = paneData;
4757
const containerRef = useRef<HTMLDivElement | null>(null);
@@ -125,8 +135,12 @@ export function TerminalPane({ ctx, workspaceId }: TerminalPaneProps) {
125135
const statPathRef = useRef(statPathMutation.mutateAsync);
126136
statPathRef.current = statPathMutation.mutateAsync;
127137

128-
const paneStoreRef = useRef(ctx.store);
129-
paneStoreRef.current = ctx.store;
138+
const onOpenFileRef = useRef(onOpenFile);
139+
onOpenFileRef.current = onOpenFile;
140+
const onRevealPathRef = useRef(onRevealPath);
141+
onRevealPathRef.current = onRevealPath;
142+
const onOpenExternalRef = useRef(onOpenExternal);
143+
onOpenExternalRef.current = onOpenExternal;
130144

131145
useEffect(() => {
132146
terminalRuntimeRegistry.setLinkHandlers(terminalId, {
@@ -145,36 +159,21 @@ export function TerminalPane({ ctx, workspaceId }: TerminalPaneProps) {
145159
return null;
146160
}
147161
},
148-
onFileLinkClick: async (event, link) => {
162+
onFileLinkClick: (event, link) => {
149163
if (!event.metaKey && !event.ctrlKey) return;
150164
event.preventDefault();
151-
const behavior =
152-
await electronTrpcClient.settings.getTerminalLinkBehavior
153-
.query()
154-
.catch(() => "file-viewer" as const);
155-
if (behavior === "file-viewer" && !link.isDirectory) {
156-
paneStoreRef.current.getState().openPane({
157-
pane: {
158-
kind: "file",
159-
data: {
160-
filePath: link.resolvedPath,
161-
mode: "editor",
162-
hasChanges: false,
163-
} satisfies FilePaneData,
164-
},
165-
});
166-
return;
167-
}
168-
electronTrpcClient.external.openFileInEditor
169-
.mutate({
170-
path: link.resolvedPath,
165+
if (event.shiftKey) {
166+
onOpenExternalRef.current(link.resolvedPath, {
171167
line: link.row,
172168
column: link.col,
173-
})
174-
.catch((error) => {
175-
console.error("[v2 Terminal] Failed to open file:", error);
176-
toast.error("Failed to open file in editor");
177169
});
170+
return;
171+
}
172+
if (link.isDirectory) {
173+
onRevealPathRef.current(link.resolvedPath);
174+
} else {
175+
onOpenFileRef.current(link.resolvedPath);
176+
}
178177
},
179178
onUrlClick: (url) => {
180179
electronTrpcClient.external.openUrl.mutate(url).catch((error) => {

apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,18 @@ function DiffViewModeToggle() {
140140
);
141141
}
142142

143+
interface UsePaneRegistryOptions {
144+
onOpenFile: (path: string, openInNewTab?: boolean) => void;
145+
onRevealPath: (path: string) => void;
146+
onOpenExternal: (
147+
path: string,
148+
opts?: { line?: number; column?: number },
149+
) => void;
150+
}
151+
143152
export function usePaneRegistry(
144153
workspaceId: string,
154+
{ onOpenFile, onRevealPath, onOpenExternal }: UsePaneRegistryOptions,
145155
): PaneRegistry<PaneViewerData> {
146156
const clearShortcut = useHotkeyDisplay("CLEAR_TERMINAL").text;
147157
const scrollToBottomShortcut = useHotkeyDisplay("SCROLL_TO_BOTTOM").text;
@@ -237,7 +247,13 @@ export function usePaneRegistry(
237247
getIcon: () => <TerminalSquare className="size-4" />,
238248
getTitle: () => "Terminal",
239249
renderPane: (ctx: RendererContext<PaneViewerData>) => (
240-
<TerminalPane ctx={ctx} workspaceId={workspaceId} />
250+
<TerminalPane
251+
ctx={ctx}
252+
workspaceId={workspaceId}
253+
onOpenFile={onOpenFile}
254+
onRevealPath={onRevealPath}
255+
onOpenExternal={onOpenExternal}
256+
/>
241257
),
242258
contextMenuActions: (_ctx, defaults) => {
243259
const terminalActions: ContextMenuActionConfig<PaneViewerData>[] = [
@@ -411,6 +427,13 @@ export function usePaneRegistry(
411427
},
412428
},
413429
}),
414-
[workspaceId, clearShortcut, scrollToBottomShortcut],
430+
[
431+
workspaceId,
432+
clearShortcut,
433+
scrollToBottomShortcut,
434+
onOpenFile,
435+
onRevealPath,
436+
onOpenExternal,
437+
],
415438
);
416439
}

apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ import {
55
ResizablePanel,
66
ResizablePanelGroup,
77
} from "@superset/ui/resizable";
8+
import { toast } from "@superset/ui/sonner";
89
import { workspaceTrpc } from "@superset/workspace-client";
910
import { eq } from "@tanstack/db";
1011
import { useLiveQuery } from "@tanstack/react-db";
1112
import { createFileRoute } from "@tanstack/react-router";
12-
import { useCallback, useMemo, useState } from "react";
13+
import { useCallback, useEffect, useMemo, useState } from "react";
1314
import { HiMiniXMark } from "react-icons/hi2";
1415
import { TbLayoutColumns, TbLayoutRows } from "react-icons/tb";
1516
import { HotkeyLabel, useHotkey } from "renderer/hotkeys";
17+
import { electronTrpcClient } from "renderer/lib/trpc-client";
1618
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
19+
import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider";
1720
import { CommandPalette } from "renderer/screens/main/components/CommandPalette";
1821
import {
1922
toAbsoluteWorkspacePath,
@@ -92,6 +95,7 @@ function WorkspaceContent({
9295
workspaceId: string;
9396
workspaceName: string;
9497
}) {
98+
const collections = useCollections();
9599
const { localWorkspaceState, store } = useV2WorkspacePaneLayout({
96100
projectId,
97101
workspaceId,
@@ -102,8 +106,6 @@ function WorkspaceContent({
102106
projectId,
103107
});
104108
useConsumePendingLaunch({ workspaceId, store });
105-
const paneRegistry = usePaneRegistry(workspaceId);
106-
const defaultContextMenuActions = useDefaultContextMenuActions(paneRegistry);
107109

108110
const workspaceQuery = workspaceTrpc.workspace.get.useQuery({
109111
id: workspaceId,
@@ -112,14 +114,40 @@ function WorkspaceContent({
112114

113115
const { recentFiles, recordView } = useRecentlyViewedFiles(workspaceId);
114116

115-
const selectedFilePath = useStore(store, (s) => {
117+
const { machineId } = useLocalHostService();
118+
const { data: workspacesWithHost = [] } = useLiveQuery(
119+
(q) =>
120+
q
121+
.from({ workspaces: collections.v2Workspaces })
122+
.leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) =>
123+
eq(workspaces.hostId, hosts.id),
124+
)
125+
.where(({ workspaces }) => eq(workspaces.id, workspaceId))
126+
.select(({ hosts }) => ({
127+
hostMachineId: hosts?.machineId ?? null,
128+
})),
129+
[collections, workspaceId],
130+
);
131+
const workspaceHost = workspacesWithHost[0];
132+
133+
const activeFilePanePath = useStore(store, (s) => {
116134
const tab = s.tabs.find((t) => t.id === s.activeTabId);
117135
if (!tab?.activePaneId) return undefined;
118136
const pane = tab.panes[tab.activePaneId];
119137
if (pane?.kind === "file") return (pane.data as FilePaneData).filePath;
120138
return undefined;
121139
});
122140

141+
const [selectedFilePath, setSelectedFilePath] = useState<string | undefined>(
142+
activeFilePanePath,
143+
);
144+
145+
useEffect(() => {
146+
if (activeFilePanePath !== undefined) {
147+
setSelectedFilePath(activeFilePanePath);
148+
}
149+
}, [activeFilePanePath]);
150+
123151
const openFilePathsKey = useStore(store, (s) =>
124152
s.tabs
125153
.flatMap((t) =>
@@ -179,6 +207,42 @@ function WorkspaceContent({
179207
[store, worktreePath, recordView],
180208
);
181209

210+
const revealPath = useCallback(
211+
(path: string) => {
212+
collections.v2WorkspaceLocalState.update(workspaceId, (draft) => {
213+
draft.rightSidebarOpen = true;
214+
});
215+
setSelectedFilePath(path);
216+
},
217+
[collections, workspaceId],
218+
);
219+
220+
const openExternal = useCallback(
221+
(path: string, opts?: { line?: number; column?: number }) => {
222+
if (workspaceHost && workspaceHost.hostMachineId !== machineId) {
223+
toast.error("Can't open remote workspace paths in an external editor");
224+
return;
225+
}
226+
electronTrpcClient.external.openFileInEditor
227+
.mutate({ path, line: opts?.line, column: opts?.column })
228+
.catch((error) => {
229+
console.error(
230+
"[v2 Terminal] Failed to open in external editor:",
231+
error,
232+
);
233+
toast.error("Failed to open in external editor");
234+
});
235+
},
236+
[workspaceHost, machineId],
237+
);
238+
239+
const paneRegistry = usePaneRegistry(workspaceId, {
240+
onOpenFile: openFilePane,
241+
onRevealPath: revealPath,
242+
onOpenExternal: openExternal,
243+
});
244+
const defaultContextMenuActions = useDefaultContextMenuActions(paneRegistry);
245+
182246
const openDiffPane = useCallback(
183247
(filePath: string) => {
184248
const state = store.getState();

0 commit comments

Comments
 (0)