Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const RECENT_STORE_LIMIT = 25;
export const RECENT_DISPLAY_LIMIT = 10;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { RECENT_DISPLAY_LIMIT, RECENT_STORE_LIMIT } from "./constants";
export {
type RecentFile,
type RecentlyViewedFilesApi,
useRecentlyViewedFiles,
} from "./useRecentlyViewedFiles";
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { eq } from "@tanstack/db";
import { useLiveQuery } from "@tanstack/react-db";
import { useCallback, useMemo } from "react";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
import { RECENT_STORE_LIMIT } from "./constants";

export interface RecentFile {
relativePath: string;
absolutePath: string;
lastAccessedAt: number;
}

interface RecentFileInput {
relativePath: string;
absolutePath: string;
}

export interface RecentlyViewedFilesApi {
recentFiles: RecentFile[];
recordView: (file: RecentFileInput) => void;
}

export function useRecentlyViewedFiles(
workspaceId: string,
): RecentlyViewedFilesApi {
const collections = useCollections();

const { data: rows = [] } = useLiveQuery(
(query) =>
query
.from({ state: collections.v2WorkspaceLocalState })
.where(({ state }) => eq(state.workspaceId, workspaceId)),
[collections, workspaceId],
);
const recentFiles = useMemo(() => rows[0]?.recentlyViewedFiles ?? [], [rows]);

const recordView = useCallback(
(file: RecentFileInput) => {
if (!collections.v2WorkspaceLocalState.get(workspaceId)) return;
collections.v2WorkspaceLocalState.update(workspaceId, (draft) => {
const current = draft.recentlyViewedFiles ?? [];
const withoutDup = current.filter(
(f) => f.relativePath !== file.relativePath,
);
draft.recentlyViewedFiles = [
{
relativePath: file.relativePath,
absolutePath: file.absolutePath,
lastAccessedAt: Date.now(),
},
...withoutDup,
].slice(0, RECENT_STORE_LIMIT);
});
},
[collections, workspaceId],
);

return { recentFiles, recordView };
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from "@superset/ui/resizable";
import { workspaceTrpc } from "@superset/workspace-client";
import { eq } from "@tanstack/db";
import { useLiveQuery } from "@tanstack/react-db";
import { createFileRoute } from "@tanstack/react-router";
Expand All @@ -14,6 +15,10 @@ import { TbLayoutColumns, TbLayoutRows } from "react-icons/tb";
import { HotkeyLabel, useHotkey } from "renderer/hotkeys";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
import { CommandPalette } from "renderer/screens/main/components/CommandPalette";
import {
toAbsoluteWorkspacePath,
toRelativeWorkspacePath,
} from "shared/absolute-paths";
import { useStore } from "zustand";
import { AddTabMenu } from "./components/AddTabMenu";
import { V2PresetsBar } from "./components/V2PresetsBar";
Expand All @@ -23,6 +28,7 @@ import { WorkspaceSidebar } from "./components/WorkspaceSidebar";
import { useDefaultContextMenuActions } from "./hooks/useDefaultContextMenuActions";
import { usePaneRegistry } from "./hooks/usePaneRegistry";
import { renderBrowserTabIcon } from "./hooks/usePaneRegistry/components/BrowserPane";
import { useRecentlyViewedFiles } from "./hooks/useRecentlyViewedFiles";
import { useV2PresetExecution } from "./hooks/useV2PresetExecution";
import { useV2WorkspacePaneLayout } from "./hooks/useV2WorkspacePaneLayout";
import { useWorkspaceHotkeys } from "./hooks/useWorkspaceHotkeys";
Expand Down Expand Up @@ -93,6 +99,13 @@ function WorkspaceContent({
const paneRegistry = usePaneRegistry(workspaceId);
const defaultContextMenuActions = useDefaultContextMenuActions(paneRegistry);

const workspaceQuery = workspaceTrpc.workspace.get.useQuery({
id: workspaceId,
});
const worktreePath = workspaceQuery.data?.worktreePath ?? "";

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

const selectedFilePath = useStore(store, (s) => {
const tab = s.tabs.find((t) => t.id === s.activeTabId);
if (!tab?.activePaneId) return undefined;
Expand All @@ -101,8 +114,29 @@ function WorkspaceContent({
return undefined;
});

const openFilePathsKey = useStore(store, (s) =>
s.tabs
.flatMap((t) =>
Object.values(t.panes)
.filter((p) => p.kind === "file")
.map((p) => (p.data as FilePaneData).filePath),
)
.join("\u0000"),
);
const openFilePaths = useMemo(
() => new Set(openFilePathsKey ? openFilePathsKey.split("\u0000") : []),
[openFilePathsKey],
);

const openFilePane = useCallback(
(filePath: string, openInNewTab?: boolean) => {
if (worktreePath) {
const absolutePath = toAbsoluteWorkspacePath(worktreePath, filePath);
const relativePath = toRelativeWorkspacePath(worktreePath, filePath);
if (relativePath && relativePath !== ".") {
recordView({ relativePath, absolutePath });
}
}
const state = store.getState();
if (openInNewTab) {
state.addTab({
Expand Down Expand Up @@ -138,7 +172,7 @@ function WorkspaceContent({
},
});
},
[store],
[store, worktreePath, recordView],
);

const openDiffPane = useCallback(
Expand Down Expand Up @@ -381,6 +415,8 @@ function WorkspaceContent({
onOpenChange={setQuickOpenOpen}
onSelectFile={openFilePane}
variant="v2"
recentlyViewedFiles={recentFiles}
openFilePaths={openFilePaths}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ export const workspaceLocalStateSchema = z.object({
paneLayout: paneWorkspaceStateSchema,
rightSidebarOpen: z.boolean().default(false),
viewedFiles: z.array(z.string()).default([]),
recentlyViewedFiles: z
.array(
z.object({
relativePath: z.string(),
absolutePath: z.string(),
lastAccessedAt: z.number(),
}),
)
.default([]),
});

export const dashboardSidebarSectionSchema = z.object({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { CommandPrimitive } from "@superset/ui/command";
import { CommandPrimitive, CommandSeparator } from "@superset/ui/command";
import { SearchIcon } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { LuChevronDown, LuChevronRight } from "react-icons/lu";
import type { RecentFile } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useRecentlyViewedFiles";
import { RECENT_DISPLAY_LIMIT } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useRecentlyViewedFiles";
import { useFileSearch } from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/hooks/useFileSearch/useFileSearch";
import { FileIcon } from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/utils";
import { FileResultItem } from "./components/FileResultItem";
import { useV2FileSearch } from "./hooks/useV2FileSearch";

// 48px input + 10 * 40px items
Expand All @@ -17,6 +19,13 @@ export interface CommandPaletteProps {
onOpenChange: (open: boolean) => void;
onSelectFile: (filePath: string) => void;
variant?: "v1" | "v2";
recentlyViewedFiles?: RecentFile[];
openFilePaths?: Set<string>;
}

function getFileName(relativePath: string): string {
const segments = relativePath.split("/");
return segments[segments.length - 1] ?? relativePath;
}

export function CommandPalette({
Expand All @@ -25,6 +34,8 @@ export function CommandPalette({
onOpenChange,
onSelectFile,
variant = "v1",
recentlyViewedFiles,
openFilePaths,
}: CommandPaletteProps) {
const [query, setQuery] = useState("");
const [filtersOpen, setFiltersOpen] = useState(false);
Expand All @@ -45,7 +56,45 @@ export function CommandPalette({
variant === "v2" ? query : "",
);

const results = variant === "v2" ? v2Search.results : v1Search.searchResults;
const rawResults =
variant === "v2" ? v2Search.results : v1Search.searchResults;
const trimmedQuery = query.trim();
const hasQuery = trimmedQuery.length > 0;
const showRecentSection = variant === "v2" && Boolean(recentlyViewedFiles);

const orderedRecent = useMemo<RecentFile[]>(() => {
if (!showRecentSection || !recentlyViewedFiles) return [];
const openSet = openFilePaths ?? new Set<string>();
const openFiles: RecentFile[] = [];
const rest: RecentFile[] = [];
for (const file of recentlyViewedFiles) {
if (openSet.has(file.absolutePath)) {
openFiles.push(file);
} else {
rest.push(file);
}
}
return [...openFiles, ...rest].slice(0, RECENT_DISPLAY_LIMIT);
}, [showRecentSection, recentlyViewedFiles, openFilePaths]);
Comment on lines +65 to +78
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Local open shadows the component's open prop

const open: RecentFile[] = [] on line 68 shadows the open: boolean prop accepted by CommandPalette. While it doesn't cause a runtime bug here (the outer open isn't needed inside this memo), the shadowing is a footgun — any future edit that needs to read the open prop inside this memo will silently get the array instead of the boolean.

Consider renaming the local variable for clarity:

Suggested change
const orderedRecent = useMemo<RecentFile[]>(() => {
if (!showRecentSection || !recentlyViewedFiles) return [];
const openSet = openFilePaths ?? new Set<string>();
const open: RecentFile[] = [];
const rest: RecentFile[] = [];
for (const file of recentlyViewedFiles) {
if (openSet.has(file.absolutePath)) {
open.push(file);
} else {
rest.push(file);
}
}
return [...open, ...rest].slice(0, RECENT_DISPLAY_LIMIT);
}, [showRecentSection, recentlyViewedFiles, openFilePaths]);
const orderedRecent = useMemo<RecentFile[]>(() => {
if (!showRecentSection || !recentlyViewedFiles) return [];
const openSet = openFilePaths ?? new Set<string>();
const openFiles: RecentFile[] = [];
const rest: RecentFile[] = [];
for (const file of recentlyViewedFiles) {
if (openSet.has(file.absolutePath)) {
openFiles.push(file);
} else {
rest.push(file);
}
}
return [...openFiles, ...rest].slice(0, RECENT_DISPLAY_LIMIT);
}, [showRecentSection, recentlyViewedFiles, openFilePaths]);


const filteredRecent = useMemo<RecentFile[]>(() => {
if (!showRecentSection) return [];
if (!hasQuery) return orderedRecent;
const needle = trimmedQuery.toLowerCase();
return orderedRecent.filter((file) =>
file.relativePath.toLowerCase().includes(needle),
);
}, [showRecentSection, hasQuery, trimmedQuery, orderedRecent]);
Comment on lines +80 to +87
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant getFileName check in filteredRecent

The second condition — getFileName(file.relativePath).toLowerCase().includes(needle) — is always a subset of the first — file.relativePath.toLowerCase().includes(needle). getFileName returns the last path segment, which is always a substring of the full relative path, so any needle that matches the filename will also match the full path. The second check can never be true when the first is false.

Suggested change
const filteredRecent = useMemo<RecentFile[]>(() => {
if (!showRecentSection) return [];
if (!hasQuery) return orderedRecent;
const needle = trimmedQuery.toLowerCase();
return orderedRecent.filter((file) => {
return (
file.relativePath.toLowerCase().includes(needle) ||
getFileName(file.relativePath).toLowerCase().includes(needle)
);
});
}, [showRecentSection, hasQuery, trimmedQuery, orderedRecent]);
const filteredRecent = useMemo<RecentFile[]>(() => {
if (!showRecentSection) return [];
if (!hasQuery) return orderedRecent;
const needle = trimmedQuery.toLowerCase();
return orderedRecent.filter((file) =>
file.relativePath.toLowerCase().includes(needle),
);
}, [showRecentSection, hasQuery, trimmedQuery, orderedRecent]);


const recentAbsSet = useMemo(
() => new Set(filteredRecent.map((f) => f.absolutePath)),
[filteredRecent],
);

const dedupedResults = useMemo(() => {
if (!showRecentSection) return rawResults;
return rawResults.filter((r) => !recentAbsSet.has(r.path));
}, [showRecentSection, rawResults, recentAbsSet]);

const handleOpenChange = useCallback(
(nextOpen: boolean) => {
Expand All @@ -67,6 +116,12 @@ export function CommandPalette({
if (open) requestAnimationFrame(() => inputRef.current?.focus());
}, [open]);

const showHeading = showRecentSection && filteredRecent.length > 0;
const showSeparator =
showRecentSection && filteredRecent.length > 0 && dedupedResults.length > 0;
const showEmptyState =
filteredRecent.length === 0 && dedupedResults.length === 0;

return (
<DialogPrimitive.Root open={open} onOpenChange={handleOpenChange} modal>
<DialogPrimitive.Portal>
Expand Down Expand Up @@ -129,32 +184,40 @@ export function CommandPalette({
)}

<CommandPrimitive.List className="max-h-[400px] overflow-x-hidden overflow-y-auto scroll-py-1 p-1">
{results.length === 0 && (
{showEmptyState && (
<CommandPrimitive.Empty className="py-6 text-center text-sm text-muted-foreground">
No files found.
</CommandPrimitive.Empty>
)}
{results.map((file) => (
<CommandPrimitive.Item

{showHeading && (
<div className="px-2 pt-2 pb-1 text-muted-foreground text-xs">
Recently Viewed
</div>
)}

{filteredRecent.map((file) => (
<FileResultItem
key={`recent:${file.absolutePath}`}
value={`recent:${file.absolutePath}`}
fileName={getFileName(file.relativePath)}
relativePath={file.relativePath}
onSelect={() => handleSelectFile(file.absolutePath)}
/>
))}

{showSeparator && (
<CommandSeparator alwaysRender className="my-1" />
)}

{dedupedResults.map((file) => (
<FileResultItem
key={file.id}
value={file.path}
fileName={file.name}
relativePath={file.relativePath}
onSelect={() => handleSelectFile(file.path)}
className="group data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-2 text-sm outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<FileIcon
fileName={file.name}
className="size-3.5 shrink-0"
/>
<span className="max-w-[252px] truncate font-medium">
{file.name}
</span>
<span className="truncate text-muted-foreground text-xs">
{file.relativePath}
</span>
<kbd className="ml-auto hidden shrink-0 text-xs text-muted-foreground group-data-[selected=true]:block">
</kbd>
</CommandPrimitive.Item>
/>
))}
</CommandPrimitive.List>
</CommandPrimitive>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { CommandPrimitive } from "@superset/ui/command";
import { FileIcon } from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/utils";

interface FileResultItemProps {
value: string;
fileName: string;
relativePath: string;
onSelect: () => void;
Comment on lines +4 to +8
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 itemKey prop adds unnecessary API surface

itemKey is passed straight through to a JSX attribute on the sole child element. Since CommandPrimitive.Item is the only thing this component renders, that reconciliation hint is a no-op — React only needs it to tell siblings apart, and there are no siblings here. The parent call sites already provide the correct reconciliation hints on the FileResultItem element itself.

Removing itemKey from FileResultItemProps and dropping its usage on CommandPrimitive.Item would simplify the component interface.

}

export function FileResultItem({
value,
fileName,
relativePath,
onSelect,
}: FileResultItemProps) {
return (
<CommandPrimitive.Item
value={value}
onSelect={onSelect}
className="group data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-2 text-sm outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<FileIcon fileName={fileName} className="size-3.5 shrink-0" />
<span className="max-w-[252px] truncate font-medium">{fileName}</span>
<span className="truncate text-muted-foreground text-xs">
{relativePath}
</span>
<kbd className="ml-auto hidden shrink-0 text-xs text-muted-foreground group-data-[selected=true]:block">
</kbd>
</CommandPrimitive.Item>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FileResultItem } from "./FileResultItem";
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading