Skip to content

feat(desktop): file tree sidebar, file pane renderers, and alert refactor#3122

Merged
saddlepaddle merged 4 commits into
mainfrom
saddlepaddle/file-tree
Apr 2, 2026
Merged

feat(desktop): file tree sidebar, file pane renderers, and alert refactor#3122
saddlepaddle merged 4 commits into
mainfrom
saddlepaddle/file-tree

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Apr 2, 2026

Summary

  • Bring back the file tree as a right sidebar (Cmd+L), persisted per workspace
  • New FilePane with content-type routing: Monaco for code, TipTap for markdown, <img> for images
  • Refactor Alert component from onConfirm/onCancel to flexible actions array API

Changes

File tree sidebar

  • New RightSidebar component with file tree, search, toolbar
  • Cmd+L toggles sidebar, state persisted in v2WorkspaceLocalState collection
  • selectedFilePath derived from store's active pane — sidebar highlights active file
  • Click file: pin if already active, else open as unpinned pane
  • useFileTree.reveal(path) expands ancestor dirs and scrolls into view
  • Fixed stale listDirectory cache (staleTime: 0)

File pane

  • FilePane orchestrator routes to CodeRenderer, MarkdownRenderer, or ImageRenderer
  • Dirty tracking via hasChanges in pane data, filled circle indicator in title
  • Italic title when unpinned, file-type icon from manifest
  • onBeforeClose on PaneDefinition — Save/Don't Save/Cancel dialog
  • onHeaderClick on PaneDefinition — click to pin, middle-click to close
  • setPanePinned no longer requires tabId

Alert component

  • New actions: AlertAction[] API replaces onConfirm/onCancel/confirmText/cancelText
  • Each action has label, variant (uses Button variants directly), onClick
  • Supports any number of buttons (Save/Don't Save/Cancel)
  • Migrated all 8 existing call sites

Other

  • Cmd+O (Open in App) wired up inside OpenInMenuButton component
  • Deleted WorkspaceFilePreview (replaced by FilePane)
  • Moved sidebar sub-components from FilesPane/ to RightSidebar/

Test plan

  • Cmd+L toggles file tree sidebar, state persists across navigation
  • Click file in sidebar → opens in pane, sidebar highlights it
  • Click same file again → pins it (title un-italicizes)
  • Edit file → dirty dot appears, close shows Save/Don't Save/Cancel dialog
  • Middle-click pane header → closes pane
  • Close tab with dirty files → batched dialog
  • Open .ts → Monaco, .md → TipTap, .png → image viewer
  • Cmd+O → opens workspace in external editor
  • New file created externally → appears in tree
  • 67 panes store tests pass

Summary by cubic

Adds a right-side file tree (Cmd+L) with per-workspace persistence and a new FilePane that renders code, markdown, and images with dirty tracking and external-change prompts. Refactors alerts to an actions API and updates pane behaviors (pin on header click, middle-click close, save prompts); the sidebar is resizable and auto-reveals and centers the active file.

  • New Features

    • Right sidebar with file tree, search, and toolbar; toggled via Cmd+L and persisted in v2WorkspaceLocalState. Highlights the active file, auto-expands ancestors, and scrolls the selection to the center; click opens or pins if already active. Resizable layout via @superset/ui/resizable.
    • FilePane: code (Monaco), markdown (TipTap), and images; handles too-large/binary; external-change bar with Reload; dirty dot in tab; italic title when unpinned; Save/Don't Save/Cancel on close (Save All supported when closing a tab). Cmd+O wired in OpenInMenuButton via useAppHotkey.
  • Refactors

    • Alert in @superset/ui: replaced confirm/cancel with actions: AlertAction[]; all call sites updated.
    • @superset/panes: added onBeforeClose and onHeaderClick; header click pins, middle-click closes; setPanePinned({ paneId, pinned }) (removed tabId); file-type icons and dirty indicator in tab headers.
    • useFileTree: added reveal() and fixed stale listDirectory cache (staleTime: 0).

Written for commit b7ccf6a. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Right sidebar for file viewing with toggle hotkey.
    • New file viewer pane: renders code, markdown (render/raw), and images; shows external-change bar with reload.
    • Pin and header interactions for panes; open-in-editor hotkey now triggers the editor.
  • Improvements

    • File tree auto-reveals and scrolls selected file into view.
    • Unified multi-action confirmation dialogs (explicit Cancel/Confirm buttons) for deletions, revokes, discards, and unsaved-change prompts.

- Add RightSidebar with file tree, toggled via Cmd+L, state persisted
  in workspace local collection
- Create FilePane with CodeRenderer (Monaco), MarkdownRenderer (TipTap),
  and ImageRenderer — replaces WorkspaceFilePreview
- File-type icons, italic title when unpinned, dirty indicator dot
- onBeforeClose on PaneDefinition — registry-level close guards with
  Save/Don't Save/Cancel dialog
- onHeaderClick on PaneDefinition — click header to pin, middle-click
  to close
- selectedFilePath derived from store, sidebar highlights active file
- Click file in sidebar: pin if already active, else open unpinned
- Cmd+O wired up in OpenInMenuButton for both old and v2 workspace
- setPanePinned: removed unnecessary tabId param
- useFileTree: added reveal() method, fixed stale cache (staleTime: 0)
- Alert component: refactored from onConfirm/onCancel to actions array
  API, migrated all 8 call sites
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Refactors alert API to an actions-based model, adds FilePane and file renderers (Code/Image/Markdown) with external-change handling, implements a right sidebar with file-tree reveal and scrolling, removes legacy WorkspaceFilePreview, adjusts pane APIs (tab-agnostic setPanePinned, onBeforeClose/onHeaderClick), and adds related hotkeys and schema fields.

Changes

Cohort / File(s) Summary
Alert core & types
packages/ui/src/atoms/Alert/Alert.tsx, packages/ui/src/atoms/Alert/index.ts
Replaced confirm/cancel API with actions: AlertAction[], removed .destructive helper, added AlertAction/AlertActionVariant/AlertOptions types and loadingIndex behavior.
Alert API migration (call sites)
apps/desktop/src/renderer/.../useDashboardSidebarProjectSectionActions/.../useDashboardSidebarProjectSectionActions.ts, .../SessionSelectorItem.tsx (two locations), apps/.../ApiKeysSettings/ApiKeysSettings.tsx, .../InviteMemberButton.tsx, .../MemberActions.tsx, .../AddSecretSheet.tsx, apps/.../SessionSelectorItem.tsx
Converted uses of alert.destructive(...confirm/onConfirm...) to alert({ actions: [...] }), replacing confirm/cancel callbacks with explicit action buttons (outline Cancel + destructive action tied to original handlers).
File viewing pane & renderers
.../FilePane/FilePane.tsx, .../FilePane/index.ts, .../ExternalChangeBar/*.ts, .../renderers/CodeRenderer/*, .../ImageRenderer/*, .../MarkdownRenderer/*
Added FilePane component and re-exports; introduced CodeRenderer, ImageRenderer, MarkdownRenderer (with view modes) and ExternalChangeBar; wired dirty tracking, reload/save callbacks, and external-change UI.
Workspace preview removal
.../WorkspaceFilePreview/*.tsx, .../WorkspaceFilePreviewContent/*.tsx, related index.ts files, FilesPane/index.ts
Removed WorkspaceFilePreview and WorkspaceFilePreviewContent modules and their re-exports; consolidated preview functionality into new FilePane/renderers.
Right sidebar & file tree integration
.../RightSidebar/RightSidebar.tsx, .../RightSidebar/index.ts, .../WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx, apps/.../page.tsx
Renamed/implemented RightSidebar, added scrollContainerRef and logic to call fileTree.reveal(selectedFilePath) then query data-filepath element and scroll into view; added resizable layout, TOGGLE_SIDEBAR hotkey and conditional mounting.
File tree API
packages/workspace-client/src/hooks/useFileTree/useFileTree.ts
Added reveal(path): Promise<void> to public result; updated directory load fetch shape and added logic to sequentially expand ancestor directories to expose a file.
Pane store & runtime changes
packages/panes/src/core/store/store.ts, packages/panes/src/core/store/store.test.ts
Removed tabId from setPanePinned signature; implementation now searches tabs for paneId and toggles pinned. Updated tests to match call-site change.
Pane runtime & header interactions
packages/panes/src/react/components/.../Pane/Pane.tsx, .../PaneHeader/PaneHeader.tsx, packages/panes/src/react/types.ts
Made close flow async and honor optional definition.onBeforeClose; context.actions.pin now calls setPanePinned({ paneId, pinned }); added onHeaderClick/onBeforeClose to PaneDefinition and wired header click/middle-click handlers.
Pane registry & workspace integration
apps/desktop/src/renderer/.../usePaneRegistry/usePaneRegistry.tsx
Registered file pane to use new FilePane, changed header title rendering to inline header with dirty indicator, enabled header-click pinning, and added onBeforeClose prompt for unsaved changes.
Local state schema
apps/desktop/src/renderer/.../dashboardSidebarLocal/schema.ts
Added rightSidebarOpen: boolean with default false to workspace local state schema.
Hotkey binding
apps/desktop/src/renderer/.../OpenInMenuButton/OpenInMenuButton.tsx
Added useAppHotkey("OPEN_IN_APP", handleOpenInEditor, ...) so the OPEN_IN_APP hotkey triggers handleOpenInEditor in addition to displaying hotkey text.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant RightSidebar
  participant FileTree
  participant Workspace (PaneRegistry)
  participant DOM

  User->>RightSidebar: Click file item
  RightSidebar->>FileTree: request reveal(selectedFilePath)
  FileTree->>FileTree: expand ancestor dirs sequentially
  FileTree-->>RightSidebar: resolved
  RightSidebar->>DOM: querySelector(`[data-filepath="..."]`)
  DOM-->>RightSidebar: element found
  RightSidebar->>DOM: scrollIntoView(element)
  RightSidebar->>Workspace: open/pin file pane (selectedFilePath)
  Workspace-->>User: Pane with file renders (FilePane -> appropriate renderer)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nibbled at alerts and made them ask,
Buttons in a row now lead the task,
Sidebars reveal and files take flight,
Renderers hum through day and night,
A little hop — the workspace feels right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the three main changes: file tree sidebar feature, file pane renderers for different content types, and the alert component refactor.
Description check ✅ Passed The PR description follows the template structure with Description, Related Issues, Type of Change, Testing, and Additional Notes sections. However, the Related Issues section is empty and Type of Change has no selection.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch saddlepaddle/file-tree

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 2, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

9 issues found across 44 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/workspace-client/src/hooks/useFileTree/useFileTree.ts">

<violation number="1" location="packages/workspace-client/src/hooks/useFileTree/useFileTree.ts:485">
P2: `reveal` uses a prefix-only path check; this can expand/load directories outside the workspace when paths share the same prefix.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/ImageRenderer.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/ImageRenderer.tsx:24">
P2: Use a cross-platform path split for the filename instead of `split("/")`.

(Based on your team's feedback about using cross-platform path utilities instead of split.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx">

<violation number="1" location="packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx:86">
P1: Handle `onBeforeClose` rejections with `try/catch` to avoid unhandled promise rejections from pane close handlers.

(Based on your team's feedback about handling async rejections in UI flows with explicit try/catch and logging.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/CodeRenderer.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/CodeRenderer.tsx:28">
P1: The content-sync condition is impossible (`!onDirtyChange` is always false), so `savedContent` never updates on new `content`, causing stale dirty-state tracking.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx:54">
P2: Avoid manual `split("/")` path parsing here; use a cross-platform basename helper so Windows paths are handled correctly.

(Based on your team's feedback about using cross-platform path utilities instead of split.) [FEEDBACK_USED]</violation>

<violation number="2" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx:63">
P1: The new “Save” action closes the pane without saving, which can drop unsaved edits. Wire this action to an actual save operation before resolving close.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/ExternalChangeBar/ExternalChangeBar.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/ExternalChangeBar/ExternalChangeBar.tsx:12">
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.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx:300">
P2: Use a cross-platform basename extraction instead of splitting only on `/`.

(Based on your team's feedback about using cross-platform path utilities instead of split.) [FEEDBACK_USED]</violation>

<violation number="2" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx:317">
P1: The “Save All” action currently closes tabs without saving. Do not resolve close-success until save is actually implemented.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +86 to +87
const allowed = await definition.onBeforeClose(pane);
if (!allowed) return;
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.

P1: Handle onBeforeClose rejections with try/catch to avoid unhandled promise rejections from pane close handlers.

(Based on your team's feedback about handling async rejections in UI flows with explicit try/catch and logging.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx, line 86:

<comment>Handle `onBeforeClose` rejections with `try/catch` to avoid unhandled promise rejections from pane close handlers.

(Based on your team's feedback about handling async rejections in UI flows with explicit try/catch and logging.) </comment>

<file context>
@@ -81,8 +81,13 @@ export function Pane<TData>({
-					store.getState().closePane({ tabId: tab.id, paneId: pane.id }),
+				close: async () => {
+					if (definition?.onBeforeClose) {
+						const allowed = await definition.onBeforeClose(pane);
+						if (!allowed) return;
+					}
</file context>
Suggested change
const allowed = await definition.onBeforeClose(pane);
if (!allowed) return;
try {
const allowed = await definition.onBeforeClose(pane);
if (!allowed) return;
} catch (err) {
console.warn("[Pane] onBeforeClose failed", err);
return;
}
Fix with Cubic

const [savedContent, setSavedContent] = useState(content);

// Track the initial/saved content to detect dirty state
if (content !== savedContent && !onDirtyChange) {
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.

P1: The content-sync condition is impossible (!onDirtyChange is always false), so savedContent never updates on new content, causing stale dirty-state tracking.

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/renderers/CodeRenderer/CodeRenderer.tsx, line 28:

<comment>The content-sync condition is impossible (`!onDirtyChange` is always false), so `savedContent` never updates on new `content`, causing stale dirty-state tracking.</comment>

<file context>
@@ -0,0 +1,59 @@
+	const [savedContent, setSavedContent] = useState(content);
+
+	// Track the initial/saved content to detect dirty state
+	if (content !== savedContent && !onDirtyChange) {
+		setSavedContent(content);
+	}
</file context>
Fix with Cubic

{
label: "Save",
onClick: () => {
// TODO: wire up save via editor ref
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.

P1: The new “Save” action closes the pane without saving, which can drop unsaved edits. Wire this action to an actual save operation before resolving close.

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/usePaneRegistry.tsx, line 63:

<comment>The new “Save” action closes the pane without saving, which can drop unsaved edits. Wire this action to an actual save operation before resolving close.</comment>

<file context>
@@ -22,20 +24,60 @@ export function usePaneRegistry(
+								{
+									label: "Save",
+									onClick: () => {
+										// TODO: wire up save via editor ref
+										resolve(true);
+									},
</file context>
Fix with Cubic

label: "Save All",
onClick: () => {
// TODO: wire up save via editor refs
resolve(true);
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.

P1: The “Save All” action currently closes tabs without saving. Do not resolve close-success until save is actually implemented.

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/page.tsx, line 317:

<comment>The “Save All” action currently closes tabs without saving. Do not resolve close-success until save is actually implemented.</comment>

<file context>
@@ -216,43 +244,110 @@ function WorkspaceContent({
+												label: "Save All",
+												onClick: () => {
+													// TODO: wire up save via editor refs
+													resolve(true);
+												},
+											},
</file context>
Fix with Cubic


const reveal = useCallback(
async (absolutePath: string): Promise<void> => {
if (!rootPath || !absolutePath.startsWith(rootPath)) return;
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: reveal uses a prefix-only path check; this can expand/load directories outside the workspace when paths share the same prefix.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/workspace-client/src/hooks/useFileTree/useFileTree.ts, line 485:

<comment>`reveal` uses a prefix-only path check; this can expand/load directories outside the workspace when paths share the same prefix.</comment>

<file context>
@@ -479,6 +480,27 @@ export function useFileTree({
 
+	const reveal = useCallback(
+		async (absolutePath: string): Promise<void> => {
+			if (!rootPath || !absolutePath.startsWith(rootPath)) return;
+
+			// Collect ancestor directories from rootPath down to the parent of the target
</file context>
Suggested change
if (!rootPath || !absolutePath.startsWith(rootPath)) return;
if (!rootPath || !isWithinPath(rootPath, absolutePath)) return;
Fix with Cubic

<div className="flex h-full items-center justify-center overflow-auto bg-background p-4">
<img
src={dataUrl}
alt={filePath.split("/").pop() ?? ""}
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: Use a cross-platform path split for the filename instead of split("/").

(Based on your team's feedback about using cross-platform path utilities instead of split.)

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/renderers/ImageRenderer/ImageRenderer.tsx, line 24:

<comment>Use a cross-platform path split for the filename instead of `split("/")`.

(Based on your team's feedback about using cross-platform path utilities instead of split.) </comment>

<file context>
@@ -0,0 +1,30 @@
+		<div className="flex h-full items-center justify-center overflow-auto bg-background p-4">
+			<img
+				src={dataUrl}
+				alt={filePath.split("/").pop() ?? ""}
+				className="max-h-full max-w-full object-contain"
+				draggable={false}
</file context>
Fix with Cubic

onBeforeClose: (pane) => {
const data = pane.data as FilePaneData;
if (!data.hasChanges) return true;
const name = data.filePath.split("/").pop();
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: Avoid manual split("/") path parsing here; use a cross-platform basename helper so Windows paths are handled correctly.

(Based on your team's feedback about using cross-platform path utilities instead of split.)

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/usePaneRegistry.tsx, line 54:

<comment>Avoid manual `split("/")` path parsing here; use a cross-platform basename helper so Windows paths are handled correctly.

(Based on your team's feedback about using cross-platform path utilities instead of split.) </comment>

<file context>
@@ -22,20 +24,60 @@ export function usePaneRegistry(
+				onBeforeClose: (pane) => {
+					const data = pane.data as FilePaneData;
+					if (!data.hasChanges) return true;
+					const name = data.filePath.split("/").pop();
+					return new Promise<boolean>((resolve) => {
+						alert({
</file context>
Suggested change
const name = data.filePath.split("/").pop();
const name = getFileName(data.filePath);
Fix with Cubic

<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

p.kind === "file" && (p.data as FilePaneData).hasChanges,
)
.map((p) =>
(p.data as FilePaneData).filePath.split("/").pop(),
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: Use a cross-platform basename extraction instead of splitting only on /.

(Based on your team's feedback about using cross-platform path utilities instead of split.)

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/page.tsx, line 300:

<comment>Use a cross-platform basename extraction instead of splitting only on `/`.

(Based on your team's feedback about using cross-platform path utilities instead of split.) </comment>

<file context>
@@ -216,43 +244,110 @@ function WorkspaceContent({
+											p.kind === "file" && (p.data as FilePaneData).hasChanges,
+									)
+									.map((p) =>
+										(p.data as FilePaneData).filePath.split("/").pop(),
+									);
+								if (dirtyFiles.length === 0) return true;
</file context>
Suggested change
(p.data as FilePaneData).filePath.split("/").pop(),
(p.data as FilePaneData).filePath.split(/[\\/]/).pop(),
Fix with Cubic

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (2)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/ImageRenderer.tsx (1)

12-16: Consider performance for large images.

The current base64 conversion iterates through each byte and builds intermediate strings, which can be slow and memory-intensive for large images. A more efficient approach uses Uint8Array directly with TextDecoder or chunked processing.

♻️ More efficient base64 encoding
-		const base64 = btoa(
-			Array.from(content)
-				.map((b) => String.fromCharCode(b))
-				.join(""),
-		);
+		// Process in chunks to avoid call stack limits and reduce memory pressure
+		let binary = "";
+		const chunkSize = 8192;
+		for (let i = 0; i < content.length; i += chunkSize) {
+			binary += String.fromCharCode(...content.subarray(i, i + chunkSize));
+		}
+		const base64 = btoa(binary);
🤖 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/renderers/ImageRenderer/ImageRenderer.tsx
around lines 12 - 16, The current byte-to-base64 conversion in ImageRenderer
(the base64 variable) is inefficient for large images because it maps every byte
to a char and joins strings; replace it with a streaming/Blob-based approach:
create a Blob from the ArrayBuffer/Uint8Array (the content), then use
FileReader.readAsDataURL or URL.createObjectURL to get an efficient data
URL/object URL for the image; update the code in the ImageRenderer where base64
is built to use the Blob + FileReader/URL.createObjectURL approach and remove
the Array.from(...).map(...).join("") path to avoid O(n) string concatenation
overhead and memory spikes.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/components/SessionSelector/components/SessionSelectorItem/SessionSelectorItem.tsx (1)

39-56: Alert migration is correct, but consider consolidating duplicate components.

The alert API migration is implemented correctly, matching the pattern used elsewhere. However, this component appears to be nearly identical to apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/SessionSelector/components/SessionSelectorItem/SessionSelectorItem.tsx.

Consider extracting the shared SessionSelectorItem into a common location to avoid maintaining duplicate code.

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/components/SessionSelector/components/SessionSelectorItem/SessionSelectorItem.tsx`
around lines 39 - 56, This component duplicates another SessionSelectorItem;
extract the shared SessionSelectorItem (including props like sessionId and
onDeleteSession and the alert/toast delete flow) into a single common module
(e.g., a shared components directory), export its prop types, update both
implementations to import the shared component, and ensure the alert call and
toast.promise logic remain identical; also reconcile any small prop or handler
differences between the two files so both callers pass the same props
(sessionId, onDeleteSession, display props) to the new shared
SessionSelectorItem.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/RightSidebar/RightSidebar.tsx:
- Around line 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).

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx:
- Around line 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.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/CodeRenderer.tsx:
- Around line 24-42: The component fails to resync its baseline when the content
prop changes; add a useEffect that runs when content changes and sets
currentContentRef.current = content, calls setSavedContent(content), and invokes
onDirtyChange(false) to reset dirty state; update references to
currentContentRef and savedContent in the effect so the editor/pane reused for a
different file or a clean reload correctly reflects the new baseline. Ensure you
reference currentContentRef, setSavedContent, and onDirtyChange in the new
effect and include content in its dependency array.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/MarkdownRenderer.tsx:
- Around line 26-40: The markdown dirty-state isn't resynced when the incoming
content prop changes; update currentContentRef and savedContent whenever content
changes and ensure onDirtyChange is called to reflect the new comparison. Add a
useEffect that runs on content (and optionally onDirtyChange) which sets
currentContentRef.current = content, setSavedContent(content), and calls
onDirtyChange(false) (or onDirtyChange(content !== savedContent) if you prefer
to preserve earlier semantics) so dirty tracking resets when the underlying
document is reloaded or the pane is reused; keep handleChange and handleSave
logic unchanged otherwise.
- Line 25: The MarkdownRenderer component currently initializes internal state
const [viewMode, _setViewMode] = useState<MarkdownViewMode>("rendered") but
never exposes or uses the setter, making the "raw" branch unreachable; update
MarkdownRenderer to accept a controlled prop (e.g., viewMode: MarkdownViewMode
and onViewModeChange: (m: MarkdownViewMode) => void) or at minimum use an
internal setter (rename _setViewMode to setViewMode) and wire the UI controls
that toggle modes to call the setter; ensure the component reads the external
prop if provided (falling back to internal state) so the "raw" rendering branch
(references to viewMode in render logic) becomes reachable and update any
related handlers/controls in this file to invoke the new onViewModeChange or
setViewMode.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx:
- Around line 61-65: The "Save" button's onClick currently immediately calls
resolve(true) (closing the pane) despite the TODO; change the handler in
usePaneRegistry's Save branch to perform the actual save via the editor ref or
save function (e.g., call the editorRef.save() or the workspace save method),
await its completion, and only call resolve(true) if the save succeeded; if the
save fails, surface an error to the user and do not resolve (keeping the pane
open). Ensure the onClick is async and references the existing editor ref or
save function instead of unconditionally calling resolve(true).

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/page.tsx:
- Around line 313-318: The "Save All" primary action currently just calls
resolve(true) in the onClick handler (the anonymous onClick that contains
resolve(true)) and therefore closes the dialog discarding unsaved edits; change
this handler to invoke the actual save flow (e.g., call a
saveDirtyFilePanes(tab) or the existing editor ref save functions) and only call
resolve(true) after the save completes successfully, or alternatively
disable/remove the "Save All" action until editor refs are wired; specifically
update the onClick for the "Save All" action (the handler that currently
contains resolve(true)) to perform saveDirtyFilePanes or the real save method
and handle success/failure before resolving.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/api-keys/components/ApiKeysSettings/ApiKeysSettings.tsx`:
- Around line 96-103: The onClick handler for the "Revoke" action calls
authClient.apiKey.delete({ keyId: id }) without error handling; wrap that async
call in a try/catch similar to handleGenerateKey, calling toast.success on
success and toast.error with a helpful message (and optionally logging the
error) on failure to avoid unhandled rejections and provide user feedback;
update the anonymous onClick async function in the ApiKeysSettings component to
perform the try/catch around authClient.apiKey.delete and use toast.error on
catch.

In
`@packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneHeader/PaneHeader.tsx`:
- Around line 64-70: The header click handlers (onClick and onAuxClick) are
firing for clicks that bubble up from nested controls (toolbar/actionsContent),
so update the handlers in PaneHeader.tsx to ignore bubbled events by checking
event.target !== event.currentTarget (or event.currentTarget.contains check) and
returning early; apply the same guard in the middle-click onAuxClick branch
before calling e.preventDefault() and onMiddleClick so clicks on nested buttons
won’t trigger header actions.

In `@packages/ui/src/atoms/Alert/Alert.tsx`:
- Around line 56-68: The dialog's passive close currently only hides the UI
(handleClose -> setIsOpen(false)) leaving any caller waiting on an alert
unresolved; update the onOpenChange/handleClose path so that when open becomes
false it also triggers the alert's caller-visible outcome: call a provided
dismissal/resolution hook on alertOptions (e.g., alertOptions.onDismiss or
alertOptions.resolve/reject) or, if none exists, invoke a sensible fallback such
as the "cancel" action callback from alertOptions.actions (use the same action
callback code path used when clicking buttons) so callers waiting on the alert
get resolved/rejected when the user passively closes the Dialog.

In `@packages/workspace-client/src/hooks/useFileTree/useFileTree.ts`:
- Around line 496-499: reveal() can resolve before an ancestor's contents are
actually populated because await expand(dir) returns immediately if
loadDirectory() sees the directory in loadingDirectories; fix by ensuring each
ancestor is fully loaded into rootEntries before proceeding: after calling
expand(dir) (or inside expand), wait for a directory-loaded signal (e.g. a new
helper like waitForDirectoryLoaded(dir) that resolves when rootEntries contains
the node or when loadingDirectories no longer contains dir), and use that helper
for each ancestor in the for loop so reveal() only continues once the node is
present in rootEntries; reference expand(), loadDirectory(), loadingDirectories,
rootEntries and reveal() when adding this wait logic.
- Around line 483-485: The reveal function's boundary check uses
absolutePath.startsWith(rootPath) which incorrectly matches sibling paths;
replace that check with the segment-aware helper isWithinPath(rootPath,
absolutePath) inside the async reveal(absolutePath: string) callback so the
function only proceeds when the path is truly inside the mounted tree (update
the conditional in reveal to call isWithinPath and return early if it returns
false).

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/ImageRenderer.tsx:
- Around line 12-16: The current byte-to-base64 conversion in ImageRenderer (the
base64 variable) is inefficient for large images because it maps every byte to a
char and joins strings; replace it with a streaming/Blob-based approach: create
a Blob from the ArrayBuffer/Uint8Array (the content), then use
FileReader.readAsDataURL or URL.createObjectURL to get an efficient data
URL/object URL for the image; update the code in the ImageRenderer where base64
is built to use the Blob + FileReader/URL.createObjectURL approach and remove
the Array.from(...).map(...).join("") path to avoid O(n) string concatenation
overhead and memory spikes.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/components/SessionSelector/components/SessionSelectorItem/SessionSelectorItem.tsx`:
- Around line 39-56: This component duplicates another SessionSelectorItem;
extract the shared SessionSelectorItem (including props like sessionId and
onDeleteSession and the alert/toast delete flow) into a single common module
(e.g., a shared components directory), export its prop types, update both
implementations to import the shared component, and ensure the alert call and
toast.promise logic remain identical; also reconcile any small prop or handler
differences between the two files so both callers pass the same props
(sessionId, onDeleteSession, display props) to the new shared
SessionSelectorItem.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4092ee5-6e1b-413d-a168-d6103d492e7f

📥 Commits

Reviewing files that changed from the base of the PR and between 9fa1c6b and a930a51.

📒 Files selected for processing (44)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/OpenInMenuButton/OpenInMenuButton.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/RightSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesSearchResultItem/WorkspaceFilesSearchResultItem.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesSearchResultItem/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesToolbar/WorkspaceFilesToolbar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesToolbar/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesTreeItem/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/hooks/useWorkspaceFileSearch/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/hooks/useWorkspaceFileSearch/useWorkspaceFileSearch.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/SessionSelector/components/SessionSelectorItem/SessionSelectorItem.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/ExternalChangeBar/ExternalChangeBar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/ExternalChangeBar/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/CodeRenderer.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/ImageRenderer.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/MarkdownRenderer.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/WorkspaceFilePreview.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/components/WorkspaceFilePreviewContent/WorkspaceFilePreviewContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/components/WorkspaceFilePreviewContent/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/api-keys/components/ApiKeysSettings/ApiKeysSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/InviteMemberButton/InviteMemberButton.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/MemberActions/MemberActions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/cloud/secrets/components/SecretsSettings/components/AddSecretSheet/AddSecretSheet.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/components/SessionSelector/components/SessionSelectorItem/SessionSelectorItem.tsx
  • packages/panes/src/core/store/store.test.ts
  • packages/panes/src/core/store/store.ts
  • packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx
  • packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneHeader/PaneHeader.tsx
  • packages/panes/src/react/types.ts
  • packages/ui/src/atoms/Alert/Alert.tsx
  • packages/ui/src/atoms/Alert/index.ts
  • packages/workspace-client/src/hooks/useFileTree/useFileTree.ts
💤 Files with no reviewable changes (6)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/components/WorkspaceFilePreviewContent/index.ts
  • packages/panes/src/core/store/store.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/WorkspaceFilePreview.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/components/WorkspaceFilePreviewContent/WorkspaceFilePreviewContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/index.ts

Comment on lines +65 to +84
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]);
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).

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

Comment on lines +24 to +42
const currentContentRef = useRef(content);
const [savedContent, setSavedContent] = useState(content);

// Track the initial/saved content to detect dirty state
if (content !== savedContent && !onDirtyChange) {
setSavedContent(content);
}

const handleChange = useCallback(
(value: string) => {
currentContentRef.current = value;
onDirtyChange(value !== savedContent);
},
[onDirtyChange, savedContent],
);

const handleSave = useCallback(async () => {
await onSave(currentContentRef.current);
setSavedContent(currentContentRef.current);
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

Resync the editor baseline when content changes.

currentContentRef and savedContent survive prop changes, and the guard on Lines 28-30 never runs because onDirtyChange is required. If this pane is reused for another text file or the document reloads cleanly, dirty tracking and handleSave can still operate on the previous content.

🩹 Suggested fix
-import { useCallback, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
@@
-	// Track the initial/saved content to detect dirty state
-	if (content !== savedContent && !onDirtyChange) {
-		setSavedContent(content);
-	}
+	useEffect(() => {
+		currentContentRef.current = content;
+		setSavedContent(content);
+		onDirtyChange(false);
+	}, [content, onDirtyChange]);
🤖 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/renderers/CodeRenderer/CodeRenderer.tsx
around lines 24 - 42, The component fails to resync its baseline when the
content prop changes; add a useEffect that runs when content changes and sets
currentContentRef.current = content, calls setSavedContent(content), and invokes
onDirtyChange(false) to reset dirty state; update references to
currentContentRef and savedContent in the effect so the editor/pane reused for a
different file or a clean reload correctly reflects the new baseline. Ensure you
reference currentContentRef, setSavedContent, and onDirtyChange in the new
effect and include content in its dependency array.

onReload,
onSave,
}: MarkdownRendererProps) {
const [viewMode, _setViewMode] = useState<MarkdownViewMode>("rendered");
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

The raw markdown mode is unreachable right now.

_setViewMode is never used, and MarkdownRenderer does not accept external view-mode state. The "raw" branch below is dead code, so users cannot actually switch out of rendered mode.

Also applies to: 46-63, 77-99

🤖 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/renderers/MarkdownRenderer/MarkdownRenderer.tsx
at line 25, The MarkdownRenderer component currently initializes internal state
const [viewMode, _setViewMode] = useState<MarkdownViewMode>("rendered") but
never exposes or uses the setter, making the "raw" branch unreachable; update
MarkdownRenderer to accept a controlled prop (e.g., viewMode: MarkdownViewMode
and onViewModeChange: (m: MarkdownViewMode) => void) or at minimum use an
internal setter (rename _setViewMode to setViewMode) and wire the UI controls
that toggle modes to call the setter; ensure the component reads the external
prop if provided (falling back to internal state) so the "raw" rendering branch
(references to viewMode in render logic) becomes reachable and update any
related handlers/controls in this file to invoke the new onViewModeChange or
setViewMode.

Comment on lines +26 to +40
const currentContentRef = useRef(content);
const [savedContent, setSavedContent] = useState(content);

const handleChange = useCallback(
(value: string) => {
currentContentRef.current = value;
onDirtyChange(value !== savedContent);
},
[onDirtyChange, savedContent],
);

const handleSave = useCallback(async () => {
await onSave(currentContentRef.current);
setSavedContent(currentContentRef.current);
}, [onSave]);
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

Resync markdown dirty state when the underlying content changes.

Like CodeRenderer, currentContentRef and savedContent are initialized once and never refreshed from content. Reloading the file or reusing the pane for another markdown document leaves dirty tracking tied to the previous version.

🤖 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/renderers/MarkdownRenderer/MarkdownRenderer.tsx
around lines 26 - 40, The markdown dirty-state isn't resynced when the incoming
content prop changes; update currentContentRef and savedContent whenever content
changes and ensure onDirtyChange is called to reflect the new comparison. Add a
useEffect that runs on content (and optionally onDirtyChange) which sets
currentContentRef.current = content, setSavedContent(content), and calls
onDirtyChange(false) (or onDirtyChange(content !== savedContent) if you prefer
to preserve earlier semantics) so dirty tracking resets when the underlying
document is reloaded or the pane is reused; keep handleChange and handleSave
logic unchanged otherwise.

Comment on lines +96 to +103
{
label: "Revoke",
variant: "destructive",
onClick: async () => {
await authClient.apiKey.delete({ keyId: id });
toast.success("API key revoked");
},
},
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 | 🟡 Minor

Add error handling for the async delete operation.

The onClick handler lacks error handling. If authClient.apiKey.delete fails, the user receives no feedback and the promise rejection may go unhandled. Compare with handleGenerateKey (lines 69-88) which properly wraps the async operation in try/catch.

🛡️ Proposed fix
 				{
 					label: "Revoke",
 					variant: "destructive",
 					onClick: async () => {
+						try {
 							await authClient.apiKey.delete({ keyId: id });
 							toast.success("API key revoked");
+						} catch (error) {
+							console.error("[api-keys] Failed to revoke API key:", error);
+							toast.error("Failed to revoke API key");
+						}
 					},
 				},
📝 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
{
label: "Revoke",
variant: "destructive",
onClick: async () => {
await authClient.apiKey.delete({ keyId: id });
toast.success("API key revoked");
},
},
{
label: "Revoke",
variant: "destructive",
onClick: async () => {
try {
await authClient.apiKey.delete({ keyId: id });
toast.success("API key revoked");
} catch (error) {
console.error("[api-keys] Failed to revoke API key:", error);
toast.error("Failed to revoke API key");
}
},
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/api-keys/components/ApiKeysSettings/ApiKeysSettings.tsx`
around lines 96 - 103, The onClick handler for the "Revoke" action calls
authClient.apiKey.delete({ keyId: id }) without error handling; wrap that async
call in a try/catch similar to handleGenerateKey, calling toast.success on
success and toast.error with a helpful message (and optionally logging the
error) on failure to avoid unhandled rejections and provide user feedback;
update the anonymous onClick async function in the ApiKeysSettings component to
perform the try/catch around authClient.apiKey.delete and use toast.error on
catch.

Comment on lines +64 to +70
onClick={onClick}
onAuxClick={(e) => {
if (e.button === 1 && onMiddleClick) {
e.preventDefault();
onMiddleClick();
}
}}
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

Ignore bubbled clicks from nested header controls.

Because these handlers sit on the outer header container, clicks from toolbar/actionsContent children also trigger the header action. In this PR that means action buttons can pin the pane, and middle-clicking a nested control can close the pane.

Possible fix
-			onClick={onClick}
-			onAuxClick={(e) => {
-				if (e.button === 1 && onMiddleClick) {
-					e.preventDefault();
-					onMiddleClick();
-				}
-			}}
+			onClick={(e) => {
+				const target = e.target as HTMLElement;
+				if (target.closest("button,a,input,textarea,select,[role='button']")) {
+					return;
+				}
+				onClick?.();
+			}}
+			onAuxClick={(e) => {
+				const target = e.target as HTMLElement;
+				if (target.closest("button,a,input,textarea,select,[role='button']")) {
+					return;
+				}
+				if (e.button === 1 && onMiddleClick) {
+					e.preventDefault();
+					onMiddleClick();
+				}
+			}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneHeader/PaneHeader.tsx`
around lines 64 - 70, The header click handlers (onClick and onAuxClick) are
firing for clicks that bubble up from nested controls (toolbar/actionsContent),
so update the handlers in PaneHeader.tsx to ignore bubbled events by checking
event.target !== event.currentTarget (or event.currentTarget.contains check) and
returning early; apply the same guard in the middle-click onAuxClick branch
before calling e.preventDefault() and onMiddleClick so clicks on nested buttons
won’t trigger header actions.

Comment on lines +56 to +68
const handleClose = () => {
setIsOpen(false);
};

if (!alertOptions) return null;

const actions = [...alertOptions.actions].reverse();

return (
<Dialog
modal={true}
open={isOpen}
onOpenChange={(open) => !open && handleCancel()}
onOpenChange={(open) => !open && handleClose()}
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

Passive dismissals need a caller-visible outcome.

handleClose() only hides the dialog. Any caller that wraps alert() in a promise now has no resolution path when onOpenChange(false) happens, so flows like the new unsaved-file prompt can get stuck waiting forever unless the user clicks one of the action buttons.

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

In `@packages/ui/src/atoms/Alert/Alert.tsx` around lines 56 - 68, The dialog's
passive close currently only hides the UI (handleClose -> setIsOpen(false))
leaving any caller waiting on an alert unresolved; update the
onOpenChange/handleClose path so that when open becomes false it also triggers
the alert's caller-visible outcome: call a provided dismissal/resolution hook on
alertOptions (e.g., alertOptions.onDismiss or alertOptions.resolve/reject) or,
if none exists, invoke a sensible fallback such as the "cancel" action callback
from alertOptions.actions (use the same action callback code path used when
clicking buttons) so callers waiting on the alert get resolved/rejected when the
user passively closes the Dialog.

Comment on lines +483 to +485
const reveal = useCallback(
async (absolutePath: string): Promise<void> => {
if (!rootPath || !absolutePath.startsWith(rootPath)) return;
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 | 🟡 Minor

Use the existing segment-aware path check here.

Line 485 uses startsWith(rootPath), which also matches sibling paths like /repo-old for a root of /repo. That lets reveal() expand and fetch directories outside the mounted tree. Reuse isWithinPath(rootPath, absolutePath) instead.

Small fix
-			if (!rootPath || !absolutePath.startsWith(rootPath)) return;
+			if (!rootPath || !isWithinPath(rootPath, absolutePath)) return;
📝 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 reveal = useCallback(
async (absolutePath: string): Promise<void> => {
if (!rootPath || !absolutePath.startsWith(rootPath)) return;
const reveal = useCallback(
async (absolutePath: string): Promise<void> => {
if (!rootPath || !isWithinPath(rootPath, absolutePath)) return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workspace-client/src/hooks/useFileTree/useFileTree.ts` around lines
483 - 485, The reveal function's boundary check uses
absolutePath.startsWith(rootPath) which incorrectly matches sibling paths;
replace that check with the segment-aware helper isWithinPath(rootPath,
absolutePath) inside the async reveal(absolutePath: string) callback so the
function only proceeds when the path is truly inside the mounted tree (update
the conditional in reveal to call isWithinPath and return early if it returns
false).

Comment on lines +496 to +499
// Expand all ancestors and load their contents
for (const dir of ancestors) {
await expand(dir);
}
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() can resolve before the ancestors are actually loaded.

await expand(dir) is not enough when that directory is already in loadingDirectories: loadDirectory() returns immediately in that case, so reveal() resolves before the node exists in rootEntries. The first reveal-and-scroll path can therefore race the initial root fetch and miss the scroll entirely.

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

In `@packages/workspace-client/src/hooks/useFileTree/useFileTree.ts` around lines
496 - 499, reveal() can resolve before an ancestor's contents are actually
populated because await expand(dir) returns immediately if loadDirectory() sees
the directory in loadingDirectories; fix by ensuring each ancestor is fully
loaded into rootEntries before proceeding: after calling expand(dir) (or inside
expand), wait for a directory-loaded signal (e.g. a new helper like
waitForDirectoryLoaded(dir) that resolves when rootEntries contains the node or
when loadingDirectories no longer contains dir), and use that helper for each
ancestor in the for loop so reveal() only continues once the node is present in
rootEntries; reference expand(), loadDirectory(), loadingDirectories,
rootEntries and reveal() when adding this wait logic.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/RightSidebar.tsx (1)

65-84: ⚠️ Potential issue | 🟠 Major

This reveal effect still skips the initial selection and can race on fast changes.

Line 66 seeds prevSelectedRef from selectedFilePath, so the first mounted selection—and the common case where rootPath becomes available after the workspace query resolves—never triggers reveal(). The async callback on Lines 74-80 also never re-checks the latest selection, so an older reveal() can recenter the sidebar after a newer click.

Suggested fix
 	const scrollContainerRef = useRef<HTMLDivElement>(null);
-	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: "center" });
-				});
-			});
-		}
-		prevSelectedRef.current = selectedFilePath;
+		if (!selectedFilePath || !rootPath) {
+			prevSelectedRef.current = undefined;
+			return;
+		}
+		if (selectedFilePath === prevSelectedRef.current) {
+			return;
+		}
+
+		const path = selectedFilePath;
+		prevSelectedRef.current = path;
+		void fileTree.reveal(path).then(() => {
+			if (prevSelectedRef.current !== path) {
+				return;
+			}
+			requestAnimationFrame(() => {
+				const el = scrollContainerRef.current?.querySelector(
+					`[data-filepath="${CSS.escape(path)}"]`,
+				);
+				el?.scrollIntoView({ block: "center" });
+			});
+		});
 	}, [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 initialized to selectedFilePath which
prevents the initial reveal and the async reveal can race; change the effect to
initialize prevSelectedRef to null (so first mount triggers) and, inside the
useEffect that calls fileTree.reveal, capture the current selection into a local
constant (e.g., const current = selectedFilePath) before awaiting the async call
and after the promise resolves verify that prevSelectedRef.current === current
(or that selectedFilePath still equals current) before calling scrollIntoView
and before updating prevSelectedRef.current; also only set
prevSelectedRef.current after a successful, still-relevant reveal and continue
to require rootPath in the trigger conditions to avoid spurious reveals.
🧹 Nitpick comments (2)
packages/panes/src/core/store/store.ts (1)

333-351: Skip no-op state updates when pin value is unchanged.

Optional polish: if pane.pinned === args.pinned, return s to avoid unnecessary updates/rerenders.

♻️ Suggested diff
 		setPanePinned: (args) => {
 			set((s) => {
 				for (const tab of s.tabs) {
 					const pane = tab.panes[args.paneId];
 					if (pane) {
+						if (pane.pinned === args.pinned) return s;
 						return {
 							tabs: s.tabs.map((t) =>
 								t.id === tab.id
 									? {
 											...t,
 											panes: {
 												...t.panes,
 												[args.paneId]: { ...pane, pinned: args.pinned },
 											},
 										}
 									: t,
 							),
 						};
 					}
 				}
 				return s;
 			});
 		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/panes/src/core/store/store.ts` around lines 333 - 351, When updating
a pane's pinned state inside the reducer that iterates over s.tabs and uses
args.paneId, first check if the found pane's current pane.pinned equals
args.pinned and, if so, return s immediately to skip the no-op update; otherwise
proceed to return the updated tabs structure (the block that maps s.tabs and
replaces the pane with { ...pane, pinned: args.pinned }). This prevents
unnecessary state replacements and re-renders when the pinned value is
unchanged.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/MarkdownRenderer.tsx (1)

70-97: MarkdownViewModeToggle is exported but not integrated.

This toggle component is defined and exported, but MarkdownRenderer doesn't use it internally (nor accept viewMode as a controlled prop). The PR summary mentions renderHeaderExtras but the wiring appears incomplete—users currently cannot switch between rendered and raw modes.

Consider either:

  1. Integrating the toggle inside MarkdownRenderer (using the unused _setViewMode)
  2. Lifting viewMode state to the parent and passing it as a controlled prop
🤖 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/renderers/MarkdownRenderer/MarkdownRenderer.tsx
around lines 70 - 97, The MarkdownViewModeToggle component is exported but not
wired into MarkdownRenderer, so users can't switch modes; either wire it as an
internal control by using the existing internal setter _setViewMode inside
MarkdownRenderer or make viewMode controlled by lifting state to the parent: add
a viewMode prop and onViewModeChange callback to MarkdownRenderer (and consume
them where MarkdownRenderer is instantiated), then render
<MarkdownViewModeToggle viewMode={viewMode} onViewModeChange={onViewModeChange}
/> (or call _setViewMode in MarkdownRenderer when toggled) and ensure
MarkdownRenderer switches between the raw and rendered branches based on the
active viewMode.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/MarkdownRenderer.tsx:
- Around line 35-38: The handleSave callback currently calls setSavedContent
unconditionally after awaiting onSave, which desynchronizes savedContent when
onSave returns a non-"saved" result (e.g., user cancelled); update handleSave to
inspect the onSave result (or catch a rejection) and only call
setSavedContent(currentContentRef.current) when the returned result indicates
success (e.g., result.status === 'saved' or a truthy success indicator). Also
ensure the onSave prop contract is adjusted (or FilePane throws on non-saved) so
the check is reliable; keep references to handleSave, onSave, currentContentRef,
and setSavedContent when making the change.

---

Duplicate comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/RightSidebar/RightSidebar.tsx:
- Around line 65-84: prevSelectedRef is initialized to selectedFilePath which
prevents the initial reveal and the async reveal can race; change the effect to
initialize prevSelectedRef to null (so first mount triggers) and, inside the
useEffect that calls fileTree.reveal, capture the current selection into a local
constant (e.g., const current = selectedFilePath) before awaiting the async call
and after the promise resolves verify that prevSelectedRef.current === current
(or that selectedFilePath still equals current) before calling scrollIntoView
and before updating prevSelectedRef.current; also only set
prevSelectedRef.current after a successful, still-relevant reveal and continue
to require rootPath in the trigger conditions to avoid spurious reveals.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/MarkdownRenderer.tsx:
- Around line 70-97: The MarkdownViewModeToggle component is exported but not
wired into MarkdownRenderer, so users can't switch modes; either wire it as an
internal control by using the existing internal setter _setViewMode inside
MarkdownRenderer or make viewMode controlled by lifting state to the parent: add
a viewMode prop and onViewModeChange callback to MarkdownRenderer (and consume
them where MarkdownRenderer is instantiated), then render
<MarkdownViewModeToggle viewMode={viewMode} onViewModeChange={onViewModeChange}
/> (or call _setViewMode in MarkdownRenderer when toggled) and ensure
MarkdownRenderer switches between the raw and rendered branches based on the
active viewMode.

In `@packages/panes/src/core/store/store.ts`:
- Around line 333-351: When updating a pane's pinned state inside the reducer
that iterates over s.tabs and uses args.paneId, first check if the found pane's
current pane.pinned equals args.pinned and, if so, return s immediately to skip
the no-op update; otherwise proceed to return the updated tabs structure (the
block that maps s.tabs and replaces the pane with { ...pane, pinned: args.pinned
}). This prevents unnecessary state replacements and re-renders when the pinned
value is unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac5fdc7b-9957-4016-b55c-77a6e1ae5c69

📥 Commits

Reviewing files that changed from the base of the PR and between a930a51 and b7ccf6a.

📒 Files selected for processing (5)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/RightSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/MarkdownRenderer.tsx
  • packages/panes/src/core/store/store.ts
  • packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx

Comment on lines +35 to +38
const handleSave = useCallback(async () => {
await onSave(currentContentRef.current);
setSavedContent(currentContentRef.current);
}, [onSave]);
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

handleSave unconditionally updates savedContent without checking save result.

When onSave returns a non-"saved" result (e.g., user cancels the dialog), this still calls setSavedContent, desyncing dirty tracking from actual file state. This is the consumer-side counterpart to the FilePane.handleSave issue.

Proposed fix
 const handleSave = useCallback(async () => {
-	await onSave(currentContentRef.current);
-	setSavedContent(currentContentRef.current);
+	const result = await onSave(currentContentRef.current);
+	if (result.status === "saved") {
+		setSavedContent(currentContentRef.current);
+	}
 }, [onSave]);

This requires updating the onSave prop type to return a result with a status field, or having FilePane throw/reject on non-saved status so setSavedContent is skipped.

🤖 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/renderers/MarkdownRenderer/MarkdownRenderer.tsx
around lines 35 - 38, The handleSave callback currently calls setSavedContent
unconditionally after awaiting onSave, which desynchronizes savedContent when
onSave returns a non-"saved" result (e.g., user cancelled); update handleSave to
inspect the onSave result (or catch a rejection) and only call
setSavedContent(currentContentRef.current) when the returned result indicates
success (e.g., result.status === 'saved' or a truthy success indicator). Also
ensure the onSave prop contract is adjusted (or FilePane throws on non-saved) so
the check is reliable; keep references to handleSave, onSave, currentContentRef,
and setSavedContent when making the change.

@saddlepaddle saddlepaddle merged commit 59426f6 into main Apr 2, 2026
14 of 15 checks passed
MocA-Love added a commit to MocA-Love/superset that referenced this pull request Apr 5, 2026
cherry-pick方式で内容を取り込み済みの14コミットをgit履歴上もマージ済みにする。

取り込み済み (cherry-pick / 手動移植):
- be22b46 superset-sh#3125 — スキップ (下記参照)
- 88bc7fb superset-sh#3127 — Revert DA1 ✓
- 92d0ff9 superset-sh#3054 — DA1 fix ✓
- c48450e superset-sh#3093 — file viewer pane fix ✓
- fffa8db superset-sh#3128 — version 1.4.7 ✓
- 589a7c7 superset-sh#3136 — fuzzy scorer (ハイブリッド方式) ✓
- ceb8c81 superset-sh#3150 — Electron 40.8.5 ✓
- 8922b94 superset-sh#3137 — terminalId分離 ✓
- c7508e5 superset-sh#3152 — GitHub無料化 ✓
- 2b91f11 superset-sh#3155 — v2 terminal theme ✓
- b8b11af superset-sh#3154 — TUI dimension fix ✓
- 7599ace superset-sh#3149 — v2 sidebar file tree (手動統合) ✓
- 4d7c612 superset-sh#3174 — DnD重複削除 ✓
- 864977d superset-sh#3157 — Host Service分離 ✓

意図的にスキップ:
- be22b46 superset-sh#3125 (GitHub polling簡素化)
  フォーク独自のGitHubSyncService (バックエンド集中ポーリング) と
  設計が異なるため不採用。upstreamはフロントエンドhover駆動、フォークは
  バックエンドキャッシュウォーマー方式。詳細は githubQueryPolicy.ts と
  github-sync-service.ts のFORK NOTEを参照。

ゴースト・マージ復元 (revert 134cfd5 で消失した内容):
- 538f306 superset-sh#3120 — Patch vuln ✓
- 1588d20 superset-sh#3108 — terminal lifecycle分離 ✓
- 59426f6 superset-sh#3122 — file tree + FilePane + Alert refactor (手動統合) ✓
- 10d9a5d superset-sh#3097 — tiptap line-height ✓
- 337a9ae superset-sh#3121 — Codex hooks削除 ✓
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant