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
Expand Up @@ -61,12 +61,18 @@ export function useDashboardSidebarProjectSectionActions({
};

const confirmRemoveFromSidebar = () => {
alert.destructive({
alert({
title: "Remove project from sidebar?",
description:
"This will remove workspaces from the sidebar and delete all project sections. The workspaces or projects won't be deleted.",
confirmText: "Remove",
onConfirm: () => removeProjectFromSidebar(project.id),
actions: [
{ label: "Cancel", variant: "outline", onClick: () => {} },
{
label: "Remove",
variant: "destructive",
onClick: () => removeProjectFromSidebar(project.id),
},
],
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from "renderer/components/OpenInExternalDropdown";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useThemeStore } from "renderer/stores";
import { useHotkeyText } from "renderer/stores/hotkeys";
import { useAppHotkey, useHotkeyText } from "renderer/stores/hotkeys";

interface OpenInMenuButtonProps {
worktreePath: string;
Expand Down Expand Up @@ -80,6 +80,10 @@ export const OpenInMenuButton = memo(function OpenInMenuButton({
copyPath.mutate(worktreePath);
}, [worktreePath, copyPath, openInApp.isPending]);

useAppHotkey("OPEN_IN_APP", handleOpenInEditor, undefined, [
handleOpenInEditor,
]);

return (
<div className="flex items-center no-drag">
{/* Main button - opens in last used app */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,27 @@ import {
useWorkspaceFsEvents,
workspaceTrpc,
} from "@superset/workspace-client";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ROW_HEIGHT,
TREE_INDENT,
} from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/constants";
import { WorkspaceFilePreview } from "./components/WorkspaceFilePreview";
import { WorkspaceFilesSearchResultItem } from "./components/WorkspaceFilesSearchResultItem";
import { WorkspaceFilesToolbar } from "./components/WorkspaceFilesToolbar";
import { WorkspaceFilesTreeItem } from "./components/WorkspaceFilesTreeItem";
import { useWorkspaceFileSearch } from "./hooks/useWorkspaceFileSearch";

interface WorkspaceFilesProps {
interface RightSidebarProps {
onSelectFile: (absolutePath: string) => void;
selectedFilePath?: string;
workspaceId: string;
}

export function FilesPane({
export function RightSidebar({
onSelectFile,
selectedFilePath,
workspaceId,
}: WorkspaceFilesProps) {
}: RightSidebarProps) {
const [isRefreshing, setIsRefreshing] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const utils = workspaceTrpc.useUtils();
Expand Down Expand Up @@ -58,12 +57,32 @@ export function FilesPane({
if (searchTerm.trim().length === 0) {
return;
}

void utils.filesystem.searchFiles.invalidate();
},
Boolean(workspaceId && searchTerm.trim().length > 0),
);

const scrollContainerRef = useRef<HTMLDivElement>(null);
const prevSelectedRef = useRef(selectedFilePath);

useEffect(() => {
if (
selectedFilePath &&
selectedFilePath !== prevSelectedRef.current &&
rootPath
) {
void fileTree.reveal(selectedFilePath).then(() => {
requestAnimationFrame(() => {
const el = scrollContainerRef.current?.querySelector(
`[data-filepath="${CSS.escape(selectedFilePath)}"]`,
);
el?.scrollIntoView({ block: "center" });
});
});
}
prevSelectedRef.current = selectedFilePath;
}, [selectedFilePath, rootPath, fileTree]);
Comment on lines +65 to +84
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.

⚠️ Potential issue | 🟠 Major

Reveal the initial selection too.

Because prevSelectedRef is seeded from selectedFilePath and updated on every effect run, the first selected file is treated as already handled. When the sidebar mounts with an active pane, the tree never expands/scrolls to that file until the selection changes again.

🩹 Suggested fix
-	const prevSelectedRef = useRef(selectedFilePath);
+	const prevSelectedRef = useRef<string | undefined>(undefined);

 	useEffect(() => {
-		if (
-			selectedFilePath &&
-			selectedFilePath !== prevSelectedRef.current &&
-			rootPath
-		) {
-			void fileTree.reveal(selectedFilePath).then(() => {
-				requestAnimationFrame(() => {
-					const el = scrollContainerRef.current?.querySelector(
-						`[data-filepath="${CSS.escape(selectedFilePath)}"]`,
-					);
-					el?.scrollIntoView({ block: "nearest" });
-				});
-			});
-		}
-		prevSelectedRef.current = selectedFilePath;
+		if (
+			!selectedFilePath ||
+			!rootPath ||
+			selectedFilePath === prevSelectedRef.current
+		) {
+			return;
+		}
+
+		void fileTree.reveal(selectedFilePath).then(() => {
+			requestAnimationFrame(() => {
+				const el = scrollContainerRef.current?.querySelector(
+					`[data-filepath="${CSS.escape(selectedFilePath)}"]`,
+				);
+				el?.scrollIntoView({ block: "nearest" });
+			});
+			prevSelectedRef.current = selectedFilePath;
+		});
 	}, [selectedFilePath, rootPath, fileTree]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const scrollContainerRef = useRef<HTMLDivElement>(null);
const prevSelectedRef = useRef(selectedFilePath);
useEffect(() => {
if (
selectedFilePath &&
selectedFilePath !== prevSelectedRef.current &&
rootPath
) {
void fileTree.reveal(selectedFilePath).then(() => {
requestAnimationFrame(() => {
const el = scrollContainerRef.current?.querySelector(
`[data-filepath="${CSS.escape(selectedFilePath)}"]`,
);
el?.scrollIntoView({ block: "nearest" });
});
});
}
prevSelectedRef.current = selectedFilePath;
}, [selectedFilePath, rootPath, fileTree]);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const prevSelectedRef = useRef<string | undefined>(undefined);
useEffect(() => {
if (
!selectedFilePath ||
!rootPath ||
selectedFilePath === prevSelectedRef.current
) {
return;
}
void fileTree.reveal(selectedFilePath).then(() => {
requestAnimationFrame(() => {
const el = scrollContainerRef.current?.querySelector(
`[data-filepath="${CSS.escape(selectedFilePath)}"]`,
);
el?.scrollIntoView({ block: "nearest" });
});
prevSelectedRef.current = selectedFilePath;
});
}, [selectedFilePath, rootPath, fileTree]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/RightSidebar/RightSidebar.tsx
around lines 65 - 84, prevSelectedRef is seeded with selectedFilePath which
prevents the initial selection from being revealed; change prevSelectedRef to
start empty (e.g., useRef<string | null>(null)) and keep the existing useEffect
comparison so that when selectedFilePath exists and prevSelectedRef.current is
null/different you call fileTree.reveal(selectedFilePath) and scroll into view
via scrollContainerRef; after the reveal set prevSelectedRef.current =
selectedFilePath so subsequent changes behave as before (references:
prevSelectedRef, selectedFilePath, useEffect, scrollContainerRef,
fileTree.reveal).


const flattenedTreeEntries = useMemo(() => {
const entries: Array<{
depth: number;
Expand Down Expand Up @@ -112,68 +131,63 @@ export function FilesPane({
}

return (
<div className="flex h-full min-h-0 overflow-hidden">
<div className="flex w-80 min-w-80 flex-col border-r border-border">
<WorkspaceFilesToolbar
isRefreshing={isRefreshing}
onCollapseAll={fileTree.collapseAll}
onNewFile={() => {}}
onNewFolder={() => {}}
onRefresh={() => void handleRefresh()}
onSearchChange={setSearchTerm}
searchTerm={searchTerm}
/>
<div className="min-h-0 flex-1 overflow-y-auto p-2">
{hasQuery ? (
searchResults.length === 0 ? (
<div className="px-2 py-3 text-sm text-muted-foreground">
{isFetchingSearch ? "Searching files..." : "No matches found"}
</div>
) : (
<div className="flex flex-col">
{searchResults.map((entry) => (
<WorkspaceFilesSearchResultItem
entry={entry}
key={entry.absolutePath}
onActivate={onSelectFile}
selectedFilePath={selectedFilePath}
/>
))}
</div>
)
) : fileTree.isLoadingRoot && fileTree.rootEntries.length === 0 ? (
<div className="flex h-full min-h-0 flex-col overflow-hidden">
<WorkspaceFilesToolbar
isRefreshing={isRefreshing}
onCollapseAll={fileTree.collapseAll}
onNewFile={() => {}}
onNewFolder={() => {}}
onRefresh={() => void handleRefresh()}
onSearchChange={setSearchTerm}
searchTerm={searchTerm}
/>
<div
ref={scrollContainerRef}
className="min-h-0 flex-1 overflow-y-auto p-2"
>
{hasQuery ? (
searchResults.length === 0 ? (
<div className="px-2 py-3 text-sm text-muted-foreground">
Loading files...
</div>
) : fileTree.rootEntries.length === 0 ? (
<div className="px-2 py-3 text-sm text-muted-foreground">
No files found
{isFetchingSearch ? "Searching files..." : "No matches found"}
</div>
) : (
<div className="flex flex-col">
{flattenedTreeEntries.map(({ depth, node }) => (
<WorkspaceFilesTreeItem
depth={depth}
indent={TREE_INDENT}
key={node.absolutePath}
node={node}
onSelectFile={onSelectFile}
onToggleDirectory={(absolutePath) =>
void fileTree.toggle(absolutePath)
}
rowHeight={ROW_HEIGHT}
{searchResults.map((entry) => (
<WorkspaceFilesSearchResultItem
entry={entry}
key={entry.absolutePath}
onActivate={onSelectFile}
selectedFilePath={selectedFilePath}
/>
))}
</div>
)}
</div>
</div>
<div className="min-h-0 flex-1">
<WorkspaceFilePreview
selectedFilePath={selectedFilePath}
workspaceId={workspaceId}
/>
)
) : fileTree.isLoadingRoot && fileTree.rootEntries.length === 0 ? (
<div className="px-2 py-3 text-sm text-muted-foreground">
Loading files...
</div>
) : fileTree.rootEntries.length === 0 ? (
<div className="px-2 py-3 text-sm text-muted-foreground">
No files found
</div>
) : (
<div className="flex flex-col">
{flattenedTreeEntries.map(({ depth, node }) => (
<WorkspaceFilesTreeItem
depth={depth}
indent={TREE_INDENT}
key={node.absolutePath}
node={node}
onSelectFile={onSelectFile}
onToggleDirectory={(absolutePath) =>
void fileTree.toggle(absolutePath)
}
rowHeight={ROW_HEIGHT}
selectedFilePath={selectedFilePath}
/>
))}
</div>
)}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function WorkspaceFilesTreeItem({

return (
<button
data-filepath={node.absolutePath}
aria-expanded={isFolder ? node.isExpanded : undefined}
className={cn(
"flex w-full cursor-pointer select-none items-center gap-1 px-1 text-left transition-colors hover:bg-accent/50",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RightSidebar } from "./RightSidebar";
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,23 @@ export function SessionSelectorItem({
className="shrink-0 rounded p-0.5 opacity-0 transition-opacity hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100"
onClick={(event) => {
event.stopPropagation();
alert.destructive({
alert({
title: "Delete Chat Session",
description: "Are you sure you want to delete this session?",
confirmText: "Delete",
onConfirm: () => {
toast.promise(onDeleteSession(sessionId), {
loading: "Deleting session...",
success: "Session deleted",
error: "Failed to delete session",
});
},
actions: [
{ label: "Cancel", variant: "outline", onClick: () => {} },
{
label: "Delete",
variant: "destructive",
onClick: () => {
toast.promise(onDeleteSession(sessionId), {
loading: "Deleting session...",
success: "Session deleted",
error: "Failed to delete session",
});
},
},
],
});
}}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { RendererContext } from "@superset/panes";
import { useFileDocument } from "@superset/workspace-client";
import { useCallback } from "react";
import { isImageFile, isMarkdownFile } from "shared/file-types";
import type { FilePaneData, PaneViewerData } from "../../../../types";
import { CodeRenderer } from "./renderers/CodeRenderer";
import { ImageRenderer } from "./renderers/ImageRenderer";
import { MarkdownRenderer } from "./renderers/MarkdownRenderer";

interface FilePaneProps {
context: RendererContext<PaneViewerData>;
workspaceId: string;
}

export function FilePane({ context, workspaceId }: FilePaneProps) {
const data = context.pane.data as FilePaneData;
const { filePath } = data;

const document = useFileDocument({
workspaceId,
absolutePath: filePath,
mode: isImageFile(filePath) ? "bytes" : "auto",
maxBytes: isImageFile(filePath) ? 10 * 1024 * 1024 : 2 * 1024 * 1024,
hasLocalChanges: data.hasChanges,
autoReloadWhenClean: true,
});

const handleDirtyChange = useCallback(
(dirty: boolean) => {
if (dirty !== data.hasChanges) {
context.actions.updateData({
...data,
hasChanges: dirty,
} as PaneViewerData);
}
},
[context.actions, data],
);

const handleSave = useCallback(
async (content: string) => {
const result = await document.save({ content });
if (result.status === "saved") {
handleDirtyChange(false);
}
return result;
},
Comment on lines +40 to +47
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.

⚠️ Potential issue | 🟠 Major

Fail the onSave contract when the document was not actually saved.

This callback resolves even when document.save() returns a non-saved status. Both renderers treat any fulfilled onSave as success and advance their local saved baseline, so soft-save failures can desync the editor’s dirty tracking from the pane state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx
around lines 40 - 47, handleSave currently resolves even when document.save()
returns a non-"saved" status, which violates the onSave contract and causes
callers to treat failures as success; update handleSave (the async callback that
calls document.save) so that it only calls handleDirtyChange(false) and returns
successfully when result.status === "saved", and otherwise rejects (throw an
Error or return Promise.reject(result)) so the onSave promise is rejected on
non-saved results.

[document, handleDirtyChange],
);

if (document.state.kind === "loading") {
return null;
}

if (document.state.kind === "not-found") {
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
File not found
</div>
);
}

if (document.state.kind === "too-large") {
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
File is too large to display
</div>
);
}

if (document.state.kind === "binary" || document.state.kind === "bytes") {
if (isImageFile(filePath) && document.state.kind === "bytes") {
return (
<ImageRenderer content={document.state.content} filePath={filePath} />
);
}
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Binary file — cannot display
</div>
);
}

if (isMarkdownFile(filePath)) {
return (
<MarkdownRenderer
content={document.state.content}
hasExternalChange={document.hasExternalChange}
onDirtyChange={handleDirtyChange}
onReload={document.reload}
onSave={handleSave}
/>
);
}

return (
<CodeRenderer
content={document.state.content}
filePath={filePath}
hasExternalChange={document.hasExternalChange}
onDirtyChange={handleDirtyChange}
onReload={document.reload}
onSave={handleSave}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
interface ExternalChangeBarProps {
onReload: () => Promise<void>;
}

export function ExternalChangeBar({ onReload }: ExternalChangeBarProps) {
return (
<div className="flex items-center gap-2 border-b border-border bg-warning/10 px-3 py-1.5 text-xs text-warning-foreground">
<span>File changed on disk.</span>
<button
type="button"
className="underline hover:no-underline"
onClick={() => void onReload()}
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 2, 2026

Choose a reason for hiding this comment

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

P2: Handle onReload() rejections in the click handler instead of discarding the promise.

(Based on your team's feedback about awaiting/catching async calls to avoid unhandled promise rejections.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/ExternalChangeBar/ExternalChangeBar.tsx, line 12:

<comment>Handle `onReload()` rejections in the click handler instead of discarding the promise.

(Based on your team's feedback about awaiting/catching async calls to avoid unhandled promise rejections.) </comment>

<file context>
@@ -0,0 +1,18 @@
+			<button
+				type="button"
+				className="underline hover:no-underline"
+				onClick={() => void onReload()}
+			>
+				Reload
</file context>
Fix with Cubic

>
Reload
</button>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ExternalChangeBar } from "./ExternalChangeBar";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FilePane } from "./FilePane";
Loading
Loading