Skip to content
Open
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
15 changes: 13 additions & 2 deletions src/components/layout/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { WorkspaceCanvasDropzone } from "@/components/workspace-canvas/Workspace
import { AssistantDropzone } from "@/components/assistant-ui/AssistantDropzone";
import { PANEL_DEFAULTS } from "@/lib/layout-constants";
import React, { useCallback, useEffect, useRef } from "react";
import { useUIStore } from "@/lib/stores/ui-store";

interface DashboardLayoutProps {
// Workspace sidebar
Expand Down Expand Up @@ -69,6 +70,10 @@ export function DashboardLayout({
maximizedItemId,
workspaceSplitViewActive = false,
}: DashboardLayoutProps) {
// Subscribe to openPanelIds for dual-panel mode detection
const openPanelIds = useUIStore((state) => state.openPanelIds);
const isDualPanel = workspaceSplitViewActive && openPanelIds.length === 2;
Comment on lines +73 to +75
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

workspaceSplitViewActive comes from props while openPanelIds is read from the store — risk of desync.

DashboardLayout receives workspaceSplitViewActive as a prop (line 71), but ModalManager subscribes to the same flag directly from the store (line 48 in ModalManager.tsx). If the parent passes a stale or different value, the two components will disagree on whether dual-panel mode is active (e.g., DashboardLayout renders the dual wrapper while ModalManager falls into the single-panel branch, or vice-versa).

Consider reading workspaceSplitViewActive from the store here as well (consistent with openPanelIds), or conversely pass it down to ModalManager as a prop — pick one source of truth.

🤖 Prompt for AI Agents
In `@src/components/layout/DashboardLayout.tsx` around lines 73 - 75,
DashboardLayout currently uses the workspaceSplitViewActive prop while
ModalManager reads it from the store, which can desync; make the source of truth
consistent by reading workspaceSplitViewActive from the store in DashboardLayout
(use the same useUIStore selector you use for openPanelIds) and compute
isDualPanel from that store value plus openPanelIds, or alternatively pass the
prop down to ModalManager — prefer the first approach: remove reliance on the
workspaceSplitViewActive prop inside DashboardLayout, import/use useUIStore to
select workspaceSplitViewActive and openPanelIds, and keep isDualPanel =
workspaceSplitViewActive && openPanelIds.length === 2 so both DashboardLayout
and ModalManager use the same store-derived value.


// Get sidebar control to auto-close when panels open
const { setOpen } = useSidebar();

Expand Down Expand Up @@ -158,8 +163,14 @@ export function DashboardLayout({
</Sidebar>

<SidebarInset className="flex flex-col relative overflow-hidden">
{/* WORKSPACE SPLIT VIEW MODE: Show workspace + item side-by-side */}
{workspaceSplitViewActive && maximizedItemId ? (
{/* DUAL-PANEL MODE: Show two item panels side-by-side */}
{isDualPanel ? (
/* ModalManager handles the ResizablePanelGroup internally for dual mode */
<div className="flex-1 flex flex-col overflow-hidden">
{modalManager}
</div>
) : workspaceSplitViewActive && maximizedItemId ? (
/* WORKSPACE SPLIT VIEW MODE: Show workspace + item side-by-side */
<ResizablePanelGroup orientation="horizontal" className="flex-1">
{/* Workspace Panel - Single Column Mode */}
<ResizablePanel
Expand Down
113 changes: 90 additions & 23 deletions src/components/modals/ModalManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PDFViewerModal from "./PDFViewerModal";
import { VersionHistoryModal } from "@/components/workspace/VersionHistoryModal";
import type { WorkspaceEvent } from "@/lib/workspace/events";
import { useUIStore } from "@/lib/stores/ui-store";
import { ResizablePanel, ResizableHandle, ResizablePanelGroup } from "@/components/ui/resizable";

interface ModalManagerProps {
// Card Detail Modal
Expand Down Expand Up @@ -67,28 +68,95 @@ export function ModalManager({

return (
<>
{/* Card Detail Modal or PDF Viewer Modal - only shown when item is maximized */}
{activeItemId && currentItem && maximizedItemId === currentItem.id && (
currentItem.type === 'pdf' ? (
<PDFViewerModal
key={currentItem.id}
item={currentItem}
isOpen={true}
onClose={() => handleClose(currentItem.id)}
onUpdateItem={(updates) => onUpdateItem(currentItem.id, updates)}
renderInline={workspaceSplitViewActive}
/>
) : (
<CardDetailModal
key={currentItem.id}
item={currentItem}
isOpen={true}
onClose={() => handleClose(currentItem.id)}
onUpdateItem={(updates) => onUpdateItem(currentItem.id, updates)}
onUpdateItemData={(updater) => onUpdateItemData(currentItem.id, updater)}
onFlushPendingChanges={onFlushPendingChanges}
renderInline={workspaceSplitViewActive}
/>
{/* Dual-Panel Mode: Render both panels when openPanelIds.length === 2 and split view is active */}
{workspaceSplitViewActive && openPanelIds.length === 2 ? (
<ResizablePanelGroup orientation="horizontal" className="h-full w-full">
{/* First Panel (Left) */}
<ResizablePanel id="dual-panel-left" defaultSize={50} minSize={30}>
Comment on lines 71 to 75
Copy link
Contributor

Choose a reason for hiding this comment

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

Resizable panels missing Group

In dual-panel mode you render <ResizablePanel> / <ResizableHandle> directly (ModalManager.tsx:71-135), but these components wrap react-resizable-panelsPanel/Separator which must be children of a Group (src/components/ui/resizable.tsx:14-21). As-is, dual-panel rendering will error at runtime. Wrap this block in <ResizablePanelGroup orientation="horizontal">…</ResizablePanelGroup> (or ensure the caller passes a Group and ModalManager only renders panels/handles within it).

{openPanelIds[0] && (() => {
const item1 = items.find(i => i.id === openPanelIds[0]);
if (!item1) return null;

return item1.type === 'pdf' ? (
<PDFViewerModal
key={item1.id}
item={item1}
isOpen={true}
onClose={() => handleClose(item1.id)}
onUpdateItem={(updates) => onUpdateItem(item1.id, updates)}
renderInline={true}
/>
) : (
<CardDetailModal
key={item1.id}
item={item1}
isOpen={true}
onClose={() => handleClose(item1.id)}
onUpdateItem={(updates) => onUpdateItem(item1.id, updates)}
onUpdateItemData={(updater) => onUpdateItemData(item1.id, updater)}
onFlushPendingChanges={onFlushPendingChanges}
renderInline={true}
/>
);
})()}
</ResizablePanel>

<ResizableHandle className="border-r border-sidebar-border" />

{/* Second Panel (Right) */}
<ResizablePanel id="dual-panel-right" defaultSize={50} minSize={30}>
{openPanelIds[1] && (() => {
const item2 = items.find(i => i.id === openPanelIds[1]);
if (!item2) return null;

return item2.type === 'pdf' ? (
<PDFViewerModal
key={item2.id}
item={item2}
isOpen={true}
onClose={() => handleClose(item2.id)}
onUpdateItem={(updates) => onUpdateItem(item2.id, updates)}
renderInline={true}
/>
) : (
<CardDetailModal
key={item2.id}
item={item2}
isOpen={true}
onClose={() => handleClose(item2.id)}
onUpdateItem={(updates) => onUpdateItem(item2.id, updates)}
onUpdateItemData={(updater) => onUpdateItemData(item2.id, updater)}
onFlushPendingChanges={onFlushPendingChanges}
renderInline={true}
/>
);
})()}
</ResizablePanel>
Comment on lines +76 to +134
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract repeated panel-rendering logic to eliminate duplication.

The left-panel block (lines 76–101) and right-panel block (lines 108–133) are nearly identical — the only difference is the index into openPanelIds. An IIFE inside JSX is also uncommon and harder to scan. Consider extracting a small helper:

Suggested refactor
+  // Render a single panel item (reused for left & right in dual mode)
+  const renderPanelItem = (panelId: string | undefined) => {
+    if (!panelId) return null;
+    const item = items.find(i => i.id === panelId);
+    if (!item) return null;
+
+    return item.type === 'pdf' ? (
+      <PDFViewerModal
+        key={item.id}
+        item={item}
+        isOpen={true}
+        onClose={() => handleClose(item.id)}
+        onUpdateItem={(updates) => onUpdateItem(item.id, updates)}
+        renderInline={true}
+      />
+    ) : (
+      <CardDetailModal
+        key={item.id}
+        item={item}
+        isOpen={true}
+        onClose={() => handleClose(item.id)}
+        onUpdateItem={(updates) => onUpdateItem(item.id, updates)}
+        onUpdateItemData={(updater) => onUpdateItemData(item.id, updater)}
+        onFlushPendingChanges={() => onFlushPendingChanges(item.id)}
+        renderInline={true}
+      />
+    );
+  };

Then the dual-panel JSX collapses to:

<ResizablePanelGroup orientation="horizontal" className="h-full w-full">
  <ResizablePanel id="dual-panel-left" defaultSize={50} minSize={30}>
    {renderPanelItem(openPanelIds[0])}
  </ResizablePanel>
  <ResizableHandle className="border-r border-sidebar-border" />
  <ResizablePanel id="dual-panel-right" defaultSize={50} minSize={30}>
    {renderPanelItem(openPanelIds[1])}
  </ResizablePanel>
</ResizablePanelGroup>
🤖 Prompt for AI Agents
In `@src/components/modals/ModalManager.tsx` around lines 76 - 134, Duplicate JSX
for left/right panels (openPanelIds[0] and openPanelIds[1]) should be extracted
into a helper to remove the IIFE and repeated logic; create a helper function
renderPanelItem(indexOrId) (or renderPanelItemByIndex) that looks up the item
from items using openPanelIds[index], returns null if missing, and returns the
same conditional JSX that currently renders PDFViewerModal or CardDetailModal
while forwarding key props (item, isOpen, onClose -> handleClose(item.id),
onUpdateItem -> onUpdateItem(item.id, ...), onUpdateItemData ->
onUpdateItemData(item.id, ...), onFlushPendingChanges, renderInline). Replace
both inlined blocks with calls to renderPanelItem(openPanelIds[0]) and
renderPanelItem(openPanelIds[1]) inside the respective ResizablePanel elements.

</ResizablePanelGroup>
Comment on lines +71 to +135
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

onFlushPendingChanges is passed without binding the item ID — flush will receive undefined.

CardDetailModal declares onFlushPendingChanges?: () => void and calls it with zero arguments. But ModalManager's prop signature is (itemId: string) => void. On lines 97 and 129 you pass it straight through, so when the modal fires the callback, itemId will be undefined and the flush will silently fail or target the wrong item.

Wrap it the same way you wrap onUpdateItem / onUpdateItemData:

Proposed fix
               <CardDetailModal
                 key={item1.id}
                 item={item1}
                 isOpen={true}
                 onClose={() => handleClose(item1.id)}
                 onUpdateItem={(updates) => onUpdateItem(item1.id, updates)}
                 onUpdateItemData={(updater) => onUpdateItemData(item1.id, updater)}
-                onFlushPendingChanges={onFlushPendingChanges}
+                onFlushPendingChanges={() => onFlushPendingChanges(item1.id)}
                 renderInline={true}
               />

Apply the same fix for the right panel (item2):

               <CardDetailModal
                 key={item2.id}
                 item={item2}
                 isOpen={true}
                 onClose={() => handleClose(item2.id)}
                 onUpdateItem={(updates) => onUpdateItem(item2.id, updates)}
                 onUpdateItemData={(updater) => onUpdateItemData(item2.id, updater)}
-                onFlushPendingChanges={onFlushPendingChanges}
+                onFlushPendingChanges={() => onFlushPendingChanges(item2.id)}
                 renderInline={true}
               />

Note: the same issue exists at Line 156 in the single-panel path, but that's pre-existing code.

🤖 Prompt for AI Agents
In `@src/components/modals/ModalManager.tsx` around lines 71 - 135, The
CardDetailModal is receiving onFlushPendingChanges without the item id so the
upstream handler (ModalManager's onFlushPendingChanges) gets undefined; update
the dual-panel left and right panel props where CardDetailModal is rendered
(references: item1, item2, CardDetailModal, onFlushPendingChanges) to wrap the
prop in an arrow that calls the manager callback with the corresponding item id
(e.g., pass a function that invokes onFlushPendingChanges(item1.id) for the left
panel and onFlushPendingChanges(item2.id) for the right panel), mirroring how
onUpdateItem/onUpdateItemData are forwarded.

) : (
/* Single Panel Mode: Card Detail Modal or PDF Viewer Modal - only shown when item is maximized */
activeItemId && currentItem && maximizedItemId === currentItem.id && (
currentItem.type === 'pdf' ? (
<PDFViewerModal
key={currentItem.id}
item={currentItem}
isOpen={true}
onClose={() => handleClose(currentItem.id)}
onUpdateItem={(updates) => onUpdateItem(currentItem.id, updates)}
renderInline={workspaceSplitViewActive}
/>
) : (
<CardDetailModal
key={currentItem.id}
item={currentItem}
isOpen={true}
onClose={() => handleClose(currentItem.id)}
onUpdateItem={(updates) => onUpdateItem(currentItem.id, updates)}
onUpdateItemData={(updater) => onUpdateItemData(currentItem.id, updater)}
onFlushPendingChanges={onFlushPendingChanges}
renderInline={workspaceSplitViewActive}
/>
)
)
)}

Expand All @@ -105,4 +173,3 @@ export function ModalManager({
</>
);
}

11 changes: 11 additions & 0 deletions src/components/workspace-canvas/WorkspaceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ function WorkspaceCard({
const setOpenModalItemId = useUIStore((state) => state.setOpenModalItemId);
const openPanel = useUIStore((state) => state.openPanel);
const closePanel = useUIStore((state) => state.closePanel);
const workspaceSplitViewActive = useUIStore((state) => state.workspaceSplitViewActive);

// Register this card's minimal context (title, id, type) with the assistant
useCardContextProvider(item);
Expand Down Expand Up @@ -1083,6 +1084,16 @@ function WorkspaceCard({
<Palette className="mr-2 h-4 w-4" />
<span>Change Color</span>
</ContextMenuItem>
{/* Double Panel Option - only show when in split view with exactly 1 panel open */}
{workspaceSplitViewActive && openPanelIds.length === 1 && !openPanelIds.includes(item.id) && (
<>
<ContextMenuSeparator />
<ContextMenuItem onSelect={() => openPanel(item.id, 'dual')}>
<Columns className="mr-2 h-4 w-4" />
<span>Double Panel</span>
</ContextMenuItem>
</>
)}
<ContextMenuSeparator />
<ContextMenuItem
onSelect={handleDelete}
Expand Down
10 changes: 7 additions & 3 deletions src/components/workspace-canvas/WorkspaceContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export default function WorkspaceContent({
// Get workspace split view state from UI store
const workspaceSplitViewActive = useUIStore((state) => state.workspaceSplitViewActive);
const maximizedItemId = useUIStore((state) => state.maximizedItemId);
const openPanelIds = useUIStore((state) => state.openPanelIds);




Expand Down Expand Up @@ -144,12 +146,14 @@ export default function WorkspaceContent({

// In workspace split view mode, exclude the currently maximized item from the grid
// so it only appears in the editor panel, not in both places
if (workspaceSplitViewActive && maximizedItemId) {
return filtered.filter(item => item.id !== maximizedItemId);
// In workspace split view mode, exclude open panels from the grid
// so they only appear in the panel area, not in both places
if (workspaceSplitViewActive && openPanelIds.length > 0) {
return filtered.filter(item => !openPanelIds.includes(item.id));
}

return filtered;
}, [viewState.items, searchQuery, activeFolderId, workspaceSplitViewActive, maximizedItemId]);
}, [viewState.items, searchQuery, activeFolderId, workspaceSplitViewActive, openPanelIds]);

// Handle opening a folder (folders are now items with type: 'folder')
const handleOpenFolder = useCallback((folderId: string) => {
Expand Down
Loading