Skip to content

feat(sidebar): add workspace sections with drag & drop reordering#2067

Merged
Kitenite merged 17 commits intosuperset-sh:mainfrom
JimmyDore:jimmy-dore/featuresidebar-ordering
Mar 7, 2026
Merged

feat(sidebar): add workspace sections with drag & drop reordering#2067
Kitenite merged 17 commits intosuperset-sh:mainfrom
JimmyDore:jimmy-dore/featuresidebar-ordering

Conversation

@JimmyDore
Copy link
Copy Markdown
Contributor

@JimmyDore JimmyDore commented Mar 5, 2026

Demo

Sections

https://www.youtube.com/watch?v=qzNTkqexMSw

Multi-Select & Batch Move

https://www.youtube.com/shorts/HZz8ifI8mJc

Summary

  • Add workspace sections: user-created groups within a project sidebar for organizing workspaces
  • Support full drag & drop: reorder workspaces within sections, move between sections, and reorder sections themselves
  • Multi-select workspaces with Cmd+click (toggle) and Shift+click (range), then drag or context-menu move as a batch
  • New workspace_sections table in local SQLite DB with Drizzle migration

Why / Context

As the number of workspaces per project grows, the flat list becomes hard to manage. Sections let users group related workspaces (e.g., by feature, sprint, or status) with collapsible headers and drag-and-drop reordering.

How It Works

Data layer:

  • New workspace_sections table (id, projectId, name, tabOrder, isCollapsed)
  • workspaces.sectionId FK (nullable, onDelete: set null) — ungrouped workspaces remain at the top
  • 7 new tRPC mutations: create/rename/delete section, toggle collapse, reorder sections, reorder workspaces within section, move workspace to section
  • moveWorkspacesToSection batch mutation for moving multiple workspaces at once (single inArray UPDATE)

UI layer:

  • WorkspaceSection component: collapsible header with context menu (rename, delete), section-level DnD via react-dnd
  • WorkspaceList extracted as shared list renderer for both ungrouped and sectioned workspaces
  • useSectionDropZone hook: native drag event handling for cross-section workspace moves with 600ms auto-expand on collapsed sections
  • active-drag-item Zustand store: bridges react-dnd drag state with native drop zone handlers
  • workspace-selection Zustand store: tracks multi-select state (Cmd+click to toggle, Shift+click to range-select)
  • MultiDragPreview custom drag layer showing "{N} workspaces" badge during multi-drag
  • "Move to Section" context menu on workspace items (batch-aware when multiple are selected)
  • getAllGrouped query now returns sections nested under each project

Manual QA Checklist

Section CRUD

  • Right-click project header → "New Section" creates a section
  • Right-click section header → "Rename Section" works
  • Double-click section header enters rename mode
  • Right-click section header → "Delete Section" removes it (workspaces return to ungrouped)
  • Section collapse/expand toggles persist after app restart

Multi-Select

  • Cmd+click toggles individual workspace selection (highlighted with ring)
  • Shift+click selects a range of workspaces (across sections within same project)
  • Cmd+click on a workspace in a different project resets selection to that project
  • Escape clears selection
  • Clicking empty sidebar space clears selection
  • Plain click navigates and clears selection

Drag & Drop — Workspaces

  • Drag workspace within a section to reorder
  • Drag workspace from ungrouped area into a section
  • Drag workspace from one section to another
  • Drag workspace out of a section to ungrouped area
  • Hovering over collapsed section auto-expands it after ~600ms
  • Multi-select then drag moves all selected workspaces to the target section
  • Multi-drag shows "{N} workspaces" badge and dims selected items

Drag & Drop — Sections

  • Drag section header to reorder sections within a project
  • Section reorder persists after app restart

Context Menu

  • Right-click workspace → "Move to Section" submenu shows all sections
  • Moving via context menu works correctly
  • Right-click with multi-selection → "Move to Section" moves all selected workspaces

Edge Cases

  • Empty sections render correctly
  • Sidebar collapsed mode still shows workspaces (sections hidden)
  • Keyboard shortcuts (Cmd+1-9) still work across sections

Testing

  • bun run typecheck — passes (pre-existing @ai-sdk/openai error on upstream, unrelated)
  • bun run lint:fix — passes

Known Limitations

  • No search/filter within sections
  • Section names are not validated for uniqueness within a project
  • Reorder operations update all tabOrder values in a loop (fine for typical section/workspace counts, not optimized for hundreds)

Follow-ups

  • Drag workspace to create a new section (auto-group)
  • Section color/icon customization
  • Persist section collapsed state per-device sync

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds workspace sections: DB migration and schema, TRPC section procedures and visual-order/reorder utils, React Query hooks and invalidation, sidebar components and DnD (sections + per-section reorder/move), a small Zustand drag-store, unit tests, and UI wiring for create/rename/delete/reorder/collapse/move operations.

Changes

Cohort / File(s) Summary
Database schema & migrations
packages/local-db/drizzle/0035_add_workspace_sections.sql, packages/local-db/drizzle/0036_add_workspaces_section_id_idx.sql, packages/local-db/drizzle/meta/0035_snapshot.json, packages/local-db/drizzle/meta/0036_snapshot.json, packages/local-db/drizzle/meta/_journal.json, packages/local-db/src/schema/schema.ts, packages/local-db/src/schema/relations.ts
Add workspace_sections table and index; add section_id to workspaces; update relations and schema types; add snapshots and journal entries.
TRPC procedures & query changes
apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts, apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts, apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
New sections procedures (create/rename/delete/reorder/toggle/move/reorderWorkspacesInSection); integrate sections into getAllGrouped and visual ordering; wire procedures into Workspaces router.
Reorder & visual-order utils + tests
apps/desktop/src/lib/trpc/routers/workspaces/utils/reorder.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/reorder.test.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.test.ts
Add reorderItems and computeNextTabOrder utilities with tests; add computeVisualOrder to compute section-aware visual workspace ordering with tests.
React Query hooks & invalidation
apps/desktop/src/renderer/react-query/workspaces/index.ts, .../useMoveWorkspaceToSection.ts, .../useReorderSections.ts, .../useReorderWorkspacesInSection.ts, .../useReorderWorkspaces.ts, .../invalidateWorkspaceQueries.ts
Add hooks wrapping section-related TRPC mutations; centralize cache invalidation via invalidateWorkspaceQueries; re-export new hooks.
Sidebar types, store & tests
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/types.ts, apps/desktop/src/renderer/stores/active-drag-item.ts, apps/desktop/src/renderer/stores/active-drag-item.test.ts
Introduce SidebarWorkspace/SidebarSection/DragItem/SectionDragItem types; add Zustand active-drag-item store and synchronous getter; tests for store.
Sidebar components & DnD
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceList/WorkspaceList.tsx, .../WorkspaceList/index.ts, .../WorkspaceListItem/WorkspaceListItem.tsx, .../WorkspaceListItem/index.ts, .../WorkspaceSection/WorkspaceSection.tsx, .../WorkspaceSection/index.ts
Add WorkspaceList and WorkspaceSection components; extend WorkspaceListItem props with sectionId/sections; implement section-aware DnD, move-to-section menu, inline rename, collapse and reorder flows; export DnD constants/types.
ProjectSection & header UI
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx, .../ProjectHeader.tsx
ProjectSection now accepts/render sections and ungrouped workspaces, computes section base indices and drop zones for cross-section reorders; ProjectHeader adds "New Section" action.
Sidebar integration & hooks
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx, .../hooks/useSectionDropZone.ts, .../hooks/index.ts
Pass sections into shortcut index aggregation; add useSectionDropZone hook for drag-over/auto-expand/move-to-section handling; re-export hook.
Tests & mocks
apps/desktop/src/lib/trpc/routers/workspaces/utils/reorder.test.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.test.ts, apps/desktop/src/test-setup.ts
Add unit tests for reorder and visual-order utilities; include workspace_sections in local DB mock.
Misc UI/provider & shortcuts
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx, apps/desktop/src/renderer/hooks/useWorkspaceShortcuts.ts, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx
Allow SKIP_ENV_VALIDATION to bypass an early-return guard; include sections when flattening workspaces for shortcuts; propagate sections through sidebar aggregation.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as ProjectSection
    participant RQ as React Query Hook
    participant TRPC as TRPC Client
    participant Backend as TRPC Backend
    participant DB as Database

    User->>UI: Click "New Section"
    UI->>RQ: createSection({ projectId, name })
    RQ->>TRPC: electronTrpc.workspaces.createSection(...)
    TRPC->>Backend: validate & compute tabOrder
    Backend->>DB: INSERT workspace_sections
    DB-->>Backend: created section
    Backend-->>TRPC: return section
    TRPC->>RQ: onSuccess -> invalidate getAllGrouped
    RQ->>TRPC: refetch getAllGrouped
    TRPC->>Backend: fetch workspaces & sections
    Backend->>DB: SELECT workspaces, sections
    DB-->>Backend: rows
    Backend-->>TRPC: grouped payload
    TRPC->>UI: updated grouped data
Loading
sequenceDiagram
    participant User
    participant UI as WorkspaceListItem
    participant Store as ActiveDragStore
    participant RQ as Reorder Hook
    participant TRPC as TRPC Client
    participant Backend as TRPC Backend
    participant DB as Database

    User->>UI: Start drag
    UI->>Store: setActiveDragItem({id, sectionId, index})
    User->>UI: Drop on target
    UI->>RQ: call reorder (project or section)
    RQ->>TRPC: electronTrpc.workspaces.reorderWorkspacesInSection(...) or reorderWorkspaces(...)
    TRPC->>Backend: validate indices, compute new tabOrders
    Backend->>DB: UPDATE workspaces.tab_order
    DB-->>Backend: success
    Backend-->>TRPC: success
    TRPC->>RQ: onSuccess -> invalidate workspace queries
    UI->>Store: clearActiveDragItem()
    UI-->>User: render new order
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through lists and tidied lanes,

New sections bloom where order reigns.
Drag, rename, move — a tidy lair,
Sidebars hum with springtime flair.
A rabbit nods: the burrow's fair.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.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 'feat(sidebar): add workspace sections with drag & drop reordering' clearly and concisely summarizes the main feature addition—workspace sections with drag-and-drop support in the sidebar.
Description check ✅ Passed The pull request description is comprehensive and follows the expected structure with clear sections for Demo, Summary, Why/Context, How It Works, Manual QA Checklist, Testing, and Known Limitations.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx (1)

67-75: ⚠️ Potential issue | 🟠 Major

Guard against undefined Electric URL when env validation is skipped.

With the new condition at Line 67, the code can reach Line 71 with unresolved/undefined env URLs and still call setElectricUrl, which risks invalid runtime configuration.

Suggested guard
 	if (useElectricCloud === undefined && !env.SKIP_ENV_VALIDATION) {
 		return null;
 	}

+	const electricUrl = useElectricCloud
+		? env.NEXT_PUBLIC_ELECTRIC_URL
+		: env.NEXT_PUBLIC_ELECTRIC_PROXY_URL;
+	if (!electricUrl) return null;
+
-	setElectricUrl(
-		useElectricCloud
-			? env.NEXT_PUBLIC_ELECTRIC_URL
-			: env.NEXT_PUBLIC_ELECTRIC_PROXY_URL,
-	);
+	setElectricUrl(electricUrl);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx`
around lines 67 - 75, The code may call setElectricUrl with undefined when env
validation is skipped; update the logic in CollectionsProvider (where
useElectricCloud is checked) to verify the chosen URL
(env.NEXT_PUBLIC_ELECTRIC_URL or env.NEXT_PUBLIC_ELECTRIC_PROXY_URL) is defined
before calling setElectricUrl—if the selected env value is undefined, either
return null or use a safe fallback and log or surface an error; ensure this
guard references useElectricCloud, setElectricUrl, and env.SKIP_ENV_VALIDATION
so the component never sets an undefined electric URL.
🧹 Nitpick comments (5)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx (1)

87-87: Avoid silent catch during workspace lookup.

Line 87 swallows all errors, which makes close-flow navigation regressions hard to diagnose. A debug log keeps this best-effort behavior while preserving traceability.

Suggested tweak
-				} catch {}
+				} catch (error) {
+					console.debug(
+						"[project-header] Failed to resolve current workspace before close",
+						error,
+					);
+				}
🤖 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/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx`
at line 87, The empty catch in the ProjectHeader component swallows errors
during the workspace lookup; update the catch block in the workspace lookup
logic inside ProjectHeader (the try/catch that currently ends with "catch {}")
to log the caught error (e.g., using the existing debug/logging utility or
processLogger) while preserving the current best-effort behavior (do not
rethrow), so failures are traceable but the UI flow remains unchanged.
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx (1)

179-199: Consider extracting shared workspace rendering to reduce branch drift.

Both branches duplicate WorkspaceList + sections.map(...) rendering with near-identical props.

Also applies to: 255-273

🤖 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/WorkspaceSidebar/ProjectSection/ProjectSection.tsx`
around lines 179 - 199, The WorkspaceList and sections.map(...) rendering is
duplicated across branches; extract the shared JSX into a single reusable
constant or small component and use it in both places. Move the block that
renders <WorkspaceList ...> (passing workspaces, shortcutBaseIndex,
sectionId=null, sections, isCollapsed=isSidebarCollapsed) and the
sections.map(...) that returns <WorkspaceSection ...> (using section.id,
projectId, sectionIndex, section.name, section.isCollapsed, section.workspaces,
shortcutBaseIndex=sectionBaseIndices.get(section.id) ?? 0, isSidebarCollapsed,
allSections=sections) into a helper (e.g., renderWorkspaces or
WorkspaceSectionsBlock) and replace the duplicated branches with a single
invocation to that helper so props like sectionBaseIndices and
isSidebarCollapsed are consistently reused.
apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspacesInSection.ts (1)

12-17: Parallelize independent invalidations in onSuccess.

These cache invalidations are independent; running them serially adds unnecessary delay.

♻️ Suggested change
 	onSuccess: async (...args) => {
-		await utils.workspaces.getAll.invalidate();
-		await utils.workspaces.getAllGrouped.invalidate();
-		await utils.workspaces.getPreviousWorkspace.invalidate();
-		await utils.workspaces.getNextWorkspace.invalidate();
+		await Promise.all([
+			utils.workspaces.getAll.invalidate(),
+			utils.workspaces.getAllGrouped.invalidate(),
+			utils.workspaces.getPreviousWorkspace.invalidate(),
+			utils.workspaces.getNextWorkspace.invalidate(),
+		]);
 		await options?.onSuccess?.(...args);
 	},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspacesInSection.ts`
around lines 12 - 17, The onSuccess handler in useReorderWorkspacesInSection is
running multiple independent cache invalidations serially
(utils.workspaces.getAll.invalidate, getAllGrouped.invalidate,
getPreviousWorkspace.invalidate, getNextWorkspace.invalidate), which slows
completion; change it to run those invalidate() calls in parallel using
Promise.all and await that Promise, then await options?.onSuccess(...args)
afterwards so the external callback still runs after invalidations complete.
Ensure you reference the onSuccess handler in useReorderWorkspacesInSection and
the four invalidate methods when making this change.
packages/local-db/drizzle/meta/0035_snapshot.json (1)

1172-1178: Add an index on workspaces.section_id for section-scale operations.

section_id is now a foreign key but lacks an index; section move/reorder/grouped queries can degrade into scans as workspace counts increase.

Also applies to: 1180-1202

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

In `@packages/local-db/drizzle/meta/0035_snapshot.json` around lines 1172 - 1178,
The workspaces table is missing an index on section_id which will cause scans
for section-scoped operations; add a non-unique index on the
workspaces.section_id column (e.g., createIndex on table "workspaces" for
["section_id"]) and update the migration/schema snapshot (0035 snapshot entry
for workspaces) so the new index appears in the JSON metadata and a
corresponding CREATE INDEX or migration step is added to the SQL/migration code
path that manages the schema.
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx (1)

561-583: Avoid no-op “Move to Section” mutations.

Current menu always allows selecting the current destination (current section or “No Section”), which triggers unnecessary mutations/invalidation.

Suggested refactor
 <ContextMenuItem
+	disabled={sectionId === null}
 	onSelect={() =>
 		moveToSection.mutate({
 			workspaceId: id,
 			sectionId: null,
 		})
 	}
 >
 	No Section
 </ContextMenuItem>
@@
 {sections.map((section) => (
 	<ContextMenuItem
 		key={section.id}
+		disabled={section.id === sectionId}
 		onSelect={() =>
 			moveToSection.mutate({
 				workspaceId: id,
 				sectionId: section.id,
 			})
 		}
 	>
🤖 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/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx`
around lines 561 - 583, Prevent no-op mutations by checking the current
destination before calling moveToSection.mutate: inside the ContextMenuItem
handlers for "No Section" and for each section (the map over sections), compare
the target sectionId (null for No Section or section.id) to the current section
identifier (e.g., currentSectionId or whatever variable holds the item's current
section) and either disable the menu item or short-circuit the onSelect to do
nothing when they match; update the ContextMenuItem props for the "No Section"
entry and for each section entry (the items created in the sections.map) to
avoid calling moveToSection.mutate if the selected destination equals the
current one.
🤖 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/lib/trpc/routers/workspaces/procedures/sections.ts`:
- Around line 58-67: deleteSection currently deletes workspaceSections but
leaves workspaces.sectionId pointing to the removed id; update workspaces first
to clear/null the foreign key then delete the section. In the deleteSection
mutation (function deleteSection) run an update on workspaces where
eq(workspaces.sectionId, input.id) to set sectionId to null (or remove the
relation), await that operation (make the mutation async if needed), then
proceed to delete from workspaceSections using the existing localDb.delete(...)
call and return success; ensure you reference workspaceSections,
workspaces.sectionId, localDb and deleteSection when making the change.
- Around line 173-185: The mutation moveWorkspaceToSection currently sets
sectionId blindly; change it to first validate the requested section (lookup in
the sections table) and ensure it either is null (allowed) or exists and belongs
to the same project as the target workspace: fetch the workspace by workspaceId
from workspaces, fetch the section by sectionId from sections (if non-null),
verify section.projectId === workspace.projectId, and only then call
localDb.update(workspaces).set({ sectionId: input.sectionId
}).where(eq(workspaces.id, input.workspaceId)).run(); if validation fails, throw
or return a descriptive error (e.g., "Section not found" or "Section belongs to
a different project") instead of performing the update.

In `@apps/desktop/src/renderer/react-query/workspaces/useReorderSections.ts`:
- Around line 12-16: The onSuccess handler in useReorderSections.ts currently
invalidates getAllGrouped, getPreviousWorkspace, and getNextWorkspace but omits
invalidating the flat cache utils.workspaces.getAll; update the onSuccess
implementation to also await utils.workspaces.getAll.invalidate() before calling
options?.onSuccess so flat consumers are refreshed after a section reorder
(refer to the onSuccess block and utils.workspaces.getAll/getAllGrouped
identifiers).

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/hooks/useSectionDropZone.ts`:
- Around line 38-51: The handleDrop path currently finalizes drop but doesn't
clear a pending auto-expand timer; inside the handleDrop callback (alongside
existing calls to getActiveDragItem, canAccept, moveToSection, setting
item.handled, dragEnterCount.current = 0 and setIsDragOver(false)) call
clearTimeout on the auto-expand timer ref (e.g. pendingAutoExpand.current or
autoExpandTimer) and set that ref to null to prevent it firing after drop;
ensure you reference the exact timer ref name used in this module and clear/null
it within handleDrop.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx`:
- Around line 179-199: The collapsed branch is missing the ungrouped drop
handlers, so add ungroupedDropZone.handlers to the container rendering the
ungrouped workspaces when isSidebarCollapsed is true (the same place the
expanded branch mounts them). Specifically, ensure the element that renders
<WorkspaceList ... /> in the collapsed branch (and/or the parent wrapper around
the mapped <WorkspaceSection /> list) spreads ungroupedDropZone.handlers so
drag-to-ungroup works; reference ungroupedDropZone.handlers, WorkspaceList and
WorkspaceSection to find and update the collapsed branch rendering logic.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx`:
- Around line 260-265: The reorder mutation is being triggered twice because
handleReorder is invoked in both the drop handler and the end handler; move the
mutation out of the drop handler and into the end handler so it runs only once.
Specifically, remove the handleReorder call from the drop handler and in the end
handler (where useActiveDragItemStore.getState().clearActiveDragItem() is
already called) call handleReorder(item) only when item is present,
!item.handled, and monitor.didDrop() is true (use monitor.didDrop() to ensure a
valid drop occurred); keep the cross-section logic in useSectionDropZone that
sets item.handled as-is.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx`:
- Around line 80-82: The section drop-zone currently allows drops across
projects because useSectionDropZone's canAccept only checks item.sectionId !==
sectionId; update the canAccept predicate used when calling useSectionDropZone
to also verify item.projectId === projectId so items from other projects are
rejected (look for useSectionDropZone call and the canAccept arrow that
references sectionId and projectId). Ensure the same project check is enforced
before invoking moveToSection if there is any later validation path.
- Around line 114-117: The drop flow currently calls commitSectionReorder twice:
once in the drop handler and again in the end handler; remove the call to
commitSectionReorder from the drop handler (the function body for the DnD drop
callback) and keep the single call in the end callback (the end: (item) => { ...
} handler) so the mutation is dispatched only once per drag operation; update
tests or comments if any reference both handlers to reflect that
commitSectionReorder is invoked solely from end.

In `@packages/local-db/drizzle/0035_add_workspace_sections.sql`:
- Line 12: The migration adds workspaces.section_id referencing
workspace_sections(id) but omits the ON DELETE SET NULL clause used in the
TypeScript schema; update the ALTER TABLE statement that creates the foreign key
for workspaces.section_id (reference to workspace_sections.id) so the FK is
created with ON DELETE SET NULL to match the TypeScript relation and ensure
deletes of workspace_sections set workspaces.section_id to NULL.

---

Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx`:
- Around line 67-75: The code may call setElectricUrl with undefined when env
validation is skipped; update the logic in CollectionsProvider (where
useElectricCloud is checked) to verify the chosen URL
(env.NEXT_PUBLIC_ELECTRIC_URL or env.NEXT_PUBLIC_ELECTRIC_PROXY_URL) is defined
before calling setElectricUrl—if the selected env value is undefined, either
return null or use a safe fallback and log or surface an error; ensure this
guard references useElectricCloud, setElectricUrl, and env.SKIP_ENV_VALIDATION
so the component never sets an undefined electric URL.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspacesInSection.ts`:
- Around line 12-17: The onSuccess handler in useReorderWorkspacesInSection is
running multiple independent cache invalidations serially
(utils.workspaces.getAll.invalidate, getAllGrouped.invalidate,
getPreviousWorkspace.invalidate, getNextWorkspace.invalidate), which slows
completion; change it to run those invalidate() calls in parallel using
Promise.all and await that Promise, then await options?.onSuccess(...args)
afterwards so the external callback still runs after invalidations complete.
Ensure you reference the onSuccess handler in useReorderWorkspacesInSection and
the four invalidate methods when making this change.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx`:
- Line 87: The empty catch in the ProjectHeader component swallows errors during
the workspace lookup; update the catch block in the workspace lookup logic
inside ProjectHeader (the try/catch that currently ends with "catch {}") to log
the caught error (e.g., using the existing debug/logging utility or
processLogger) while preserving the current best-effort behavior (do not
rethrow), so failures are traceable but the UI flow remains unchanged.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx`:
- Around line 179-199: The WorkspaceList and sections.map(...) rendering is
duplicated across branches; extract the shared JSX into a single reusable
constant or small component and use it in both places. Move the block that
renders <WorkspaceList ...> (passing workspaces, shortcutBaseIndex,
sectionId=null, sections, isCollapsed=isSidebarCollapsed) and the
sections.map(...) that returns <WorkspaceSection ...> (using section.id,
projectId, sectionIndex, section.name, section.isCollapsed, section.workspaces,
shortcutBaseIndex=sectionBaseIndices.get(section.id) ?? 0, isSidebarCollapsed,
allSections=sections) into a helper (e.g., renderWorkspaces or
WorkspaceSectionsBlock) and replace the duplicated branches with a single
invocation to that helper so props like sectionBaseIndices and
isSidebarCollapsed are consistently reused.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx`:
- Around line 561-583: Prevent no-op mutations by checking the current
destination before calling moveToSection.mutate: inside the ContextMenuItem
handlers for "No Section" and for each section (the map over sections), compare
the target sectionId (null for No Section or section.id) to the current section
identifier (e.g., currentSectionId or whatever variable holds the item's current
section) and either disable the menu item or short-circuit the onSelect to do
nothing when they match; update the ContextMenuItem props for the "No Section"
entry and for each section entry (the items created in the sections.map) to
avoid calling moveToSection.mutate if the selected destination equals the
current one.

In `@packages/local-db/drizzle/meta/0035_snapshot.json`:
- Around line 1172-1178: The workspaces table is missing an index on section_id
which will cause scans for section-scoped operations; add a non-unique index on
the workspaces.section_id column (e.g., createIndex on table "workspaces" for
["section_id"]) and update the migration/schema snapshot (0035 snapshot entry
for workspaces) so the new index appears in the JSON metadata and a
corresponding CREATE INDEX or migration step is added to the SQL/migration code
path that manages the schema.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 714650a6-45ca-46de-8f98-6df0ff3a13c8

📥 Commits

Reviewing files that changed from the base of the PR and between 4b5595d and a8ddc8e.

📒 Files selected for processing (27)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/renderer/react-query/workspaces/index.ts
  • apps/desktop/src/renderer/react-query/workspaces/useMoveWorkspaceToSection.ts
  • apps/desktop/src/renderer/react-query/workspaces/useReorderSections.ts
  • apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts
  • apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspacesInSection.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceList/WorkspaceList.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceList/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/hooks/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/hooks/useSectionDropZone.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/types.ts
  • apps/desktop/src/renderer/stores/active-drag-item.ts
  • packages/local-db/drizzle/0035_add_workspace_sections.sql
  • packages/local-db/drizzle/meta/0035_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/src/schema/relations.ts
  • packages/local-db/src/schema/schema.ts
💤 Files with no reviewable changes (1)
  • apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts

Comment thread apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts
Comment thread apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts
Comment thread packages/local-db/drizzle/0035_add_workspace_sections.sql Outdated
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.

10 issues found across 27 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="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx:87">
P2: Avoid swallowing errors with an empty `catch {}`; log a warning with context so navigation issues can be diagnosed.

(Based on your team's feedback about avoiding empty catch blocks that hide failures.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx:260">
P1: Same-section drag/drop can trigger duplicate reorder mutations because `end` calls `handleReorder` even after a successful drop.</violation>
</file>

<file name="apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts">

<violation number="1" location="apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts:40">
P2: Workspaces with stale/unknown `sectionId` are excluded from sidebar visual order, which can break previous/next workspace navigation for those items.</violation>
</file>

<file name="apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspacesInSection.ts">

<violation number="1" location="apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspacesInSection.ts:3">
P2: This new hook duplicates existing workspace cache-invalidation business logic; extract the shared invalidation flow into a common helper/hook to prevent behavior drift across reorder mutations.

(Based on your team's feedback about avoiding duplicate business logic.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx:81">
P1: Section drop acceptance is missing a same-project guard, so workspaces can be dropped into sections from other projects and persisted with inconsistent project/section linkage.</violation>

<violation number="2" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx:114">
P1: Section reorder is executed twice (in `drop` and again in drag `end`), which can apply `fromIndex`/`toIndex` twice and produce incorrect final ordering.</violation>
</file>

<file name="packages/local-db/src/schema/schema.ts">

<violation number="1" location="packages/local-db/src/schema/schema.ts:124">
P2: Add an index for `workspaces.sectionId` to avoid unnecessary full-table scans during section FK maintenance and section-based lookups.</violation>
</file>

<file name="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx:30">
P2: Shortcut base indices now include sectioned workspaces, but keyboard navigation still flattens only ungrouped workspaces, so Cmd+N labels and actual targets can diverge.</violation>
</file>

<file name="packages/local-db/drizzle/0035_add_workspace_sections.sql">

<violation number="1" location="packages/local-db/drizzle/0035_add_workspace_sections.sql:12">
P1: Add `ON DELETE SET NULL` to the `workspaces.section_id` foreign key. Without it, deleting a section can fail due to FK `NO ACTION` instead of ungrouping attached workspaces.</violation>
</file>

<file name="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/hooks/useSectionDropZone.ts">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/hooks/useSectionDropZone.ts:49">
P2: Clear the pending auto-expand timeout in `handleDrop`; otherwise a collapsed section can auto-expand after the drop has already completed.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

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

Comment thread packages/local-db/drizzle/0035_add_workspace_sections.sql Outdated
Comment thread apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts Outdated
Comment thread packages/local-db/src/schema/schema.ts
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.

♻️ Duplicate comments (1)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts (1)

153-168: ⚠️ Potential issue | 🟠 Major

Validate that target section belongs to the same project as the workspace.

The mutation sets sectionId without verifying that the section exists or belongs to the workspace's project. This could create cross-project workspace-section relationships.

🛡️ Proposed fix with validation
 		moveWorkspaceToSection: publicProcedure
 			.input(
 				z.object({
 					workspaceId: z.string(),
 					sectionId: z.string().nullable(),
 				}),
 			)
 			.mutation(({ input }) => {
+				const workspace = localDb
+					.select()
+					.from(workspaces)
+					.where(eq(workspaces.id, input.workspaceId))
+					.get();
+				if (!workspace) {
+					throw new Error(`Workspace ${input.workspaceId} not found`);
+				}
+
+				if (input.sectionId) {
+					const section = localDb
+						.select()
+						.from(workspaceSections)
+						.where(eq(workspaceSections.id, input.sectionId))
+						.get();
+					if (!section || section.projectId !== workspace.projectId) {
+						throw new Error("Section not found or belongs to a different project");
+					}
+				}
+
 				localDb
 					.update(workspaces)
 					.set({ sectionId: input.sectionId })
 					.where(eq(workspaces.id, input.workspaceId))
 					.run();

 				return { success: true };
 			}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts` around
lines 153 - 168, In moveWorkspaceToSection validate the target section belongs
to the same project as the workspace before updating: fetch the workspace by
workspaces.id and if input.sectionId is non-null fetch the section by
sections.id, confirm section.projectId === workspace.projectId, and if not found
or mismatched throw an appropriate error; only then call
localDb.update(workspaces).set({ sectionId: input.sectionId
}).where(eq(workspaces.id, input.workspaceId)).run(); allow input.sectionId ===
null to clear the section without validation.
🧹 Nitpick comments (2)
apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts (1)

36-58: Consider pre-grouping workspaces by sectionId for efficiency.

The current implementation filters projectWorkspaces multiple times: once for ungrouped (Line 41-45) and once per section (Lines 52-56). For projects with many sections, this becomes O(workspaces × sections).

♻️ Optional optimization using a Map
 	for (const project of activeProjects) {
 		const projectWorkspaces = workspaces
 			.filter((w) => w.projectId === project.id)
 			.sort((a, b) => a.tabOrder - b.tabOrder);

+		// Group workspaces by sectionId once
+		const workspacesBySectionId = new Map<string | null, typeof projectWorkspaces>();
+		for (const ws of projectWorkspaces) {
+			const key = ws.sectionId;
+			if (!workspacesBySectionId.has(key)) {
+				workspacesBySectionId.set(key, []);
+			}
+			workspacesBySectionId.get(key)!.push(ws);
+		}
+
-		for (const ws of projectWorkspaces.filter(
-			(w) => w.sectionId === null,
-		)) {
+		for (const ws of workspacesBySectionId.get(null) ?? []) {
 			orderedIds.push(ws.id);
 		}

 		const projectSections = sections
 			.filter((s) => s.projectId === project.id)
 			.sort((a, b) => a.tabOrder - b.tabOrder);

 		for (const section of projectSections) {
-			for (const ws of projectWorkspaces.filter(
-				(w) => w.sectionId === section.id,
-			)) {
+			for (const ws of workspacesBySectionId.get(section.id) ?? []) {
 				orderedIds.push(ws.id);
 			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts` around
lines 36 - 58, The loop repeatedly filters projectWorkspaces causing
O(workspaces × sections) cost; fix visual ordering by pre-grouping workspaces by
sectionId (including null) once per project into a Map keyed by sectionId, then
push ids for map.get(null) first and iterate projectSections (from sections
sorted by tabOrder) to push map.get(section.id) entries; update references to
projectWorkspaces/projectSections in the function that builds orderedIds so
filtering is removed and lookups use the Map for O(1) access.
apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts (1)

69-98: Loop-based updates are acceptable for now, but consider batching for scale.

Both reorderSections and reorderWorkspacesInSection update each item individually in a loop. This is noted as a known limitation in the PR description. For small counts this is fine, but a single batch update (via SQL CASE WHEN or transaction batching) would be more efficient at scale.

Also applies to: 122-151

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

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts` around
lines 69 - 98, The current implementations of reorderSections and
reorderWorkspacesInSection perform per-item updates inside a loop (iterating
over sections/workspaces and calling localDb.update(...) for each), which is
inefficient at scale; replace the looped single-row updates with a batched
update—either construct a single UPDATE using a CASE WHEN mapping each id to its
new tabOrder for workspaceSections and the corresponding table (e.g.,
workspaceWorkspaces), or execute the updates inside a single transaction/batch
call provided by localDb so all tabOrder changes are applied in one round-trip;
update the logic inside the reorderSections and reorderWorkspacesInSection
procedures to build and run that batch/CASE update instead of calling
localDb.update per item.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts`:
- Around line 153-168: In moveWorkspaceToSection validate the target section
belongs to the same project as the workspace before updating: fetch the
workspace by workspaces.id and if input.sectionId is non-null fetch the section
by sections.id, confirm section.projectId === workspace.projectId, and if not
found or mismatched throw an appropriate error; only then call
localDb.update(workspaces).set({ sectionId: input.sectionId
}).where(eq(workspaces.id, input.workspaceId)).run(); allow input.sectionId ===
null to clear the section without validation.

---

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts`:
- Around line 69-98: The current implementations of reorderSections and
reorderWorkspacesInSection perform per-item updates inside a loop (iterating
over sections/workspaces and calling localDb.update(...) for each), which is
inefficient at scale; replace the looped single-row updates with a batched
update—either construct a single UPDATE using a CASE WHEN mapping each id to its
new tabOrder for workspaceSections and the corresponding table (e.g.,
workspaceWorkspaces), or execute the updates inside a single transaction/batch
call provided by localDb so all tabOrder changes are applied in one round-trip;
update the logic inside the reorderSections and reorderWorkspacesInSection
procedures to build and run that batch/CASE update instead of calling
localDb.update per item.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts`:
- Around line 36-58: The loop repeatedly filters projectWorkspaces causing
O(workspaces × sections) cost; fix visual ordering by pre-grouping workspaces by
sectionId (including null) once per project into a Map keyed by sectionId, then
push ids for map.get(null) first and iterate projectSections (from sections
sorted by tabOrder) to push map.get(section.id) entries; update references to
projectWorkspaces/projectSections in the function that builds orderedIds so
filtering is removed and lookups use the Map for O(1) access.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f09d9a2c-808a-4674-bf8a-665e1c889971

📥 Commits

Reviewing files that changed from the base of the PR and between a8ddc8e and 59513be.

📒 Files selected for processing (8)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/reorder.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/reorder.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts
  • apps/desktop/src/renderer/stores/active-drag-item.test.ts
  • apps/desktop/test-setup.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/desktop/src/renderer/stores/active-drag-item.test.ts

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.

7 issues found across 33 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="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx:87">
P2: Avoid using an empty `catch {}` here; it silently hides failures in a navigation-critical async path. Log a warning (with context) at minimum so operational issues are observable.

(Based on your team's feedback about avoiding empty catch blocks that hide failures.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspacesInSection.ts">

<violation number="1" location="apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspacesInSection.ts:12">
P3: This onSuccess invalidation block duplicates the same workspace cache invalidations already used in useReorderWorkspaces. Consider extracting a shared helper/hook to avoid drift and keep cache invalidation logic consistent.

(Based on your team's feedback about avoiding duplicated business logic.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx:30">
P2: Shortcut indices now count sectioned workspaces, but the hotkey navigation list still excludes sectioned items. This makes the displayed ⌘1–9 labels inconsistent with what the shortcuts actually open when sections are present. Update the hotkey workspace list to include section workspaces in the same order as the UI.</violation>
</file>

<file name="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx:221">
P2: Optimistic reorder cache updates are never reverted on mutation failure, so a failed reorder can leave the sidebar in the wrong order. Add an onSettled invalidation (as before) to restore server state on errors.</violation>

<violation number="2" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx:260">
P1: `handleReorder` is now invoked in both `drop` and `end`, so a normal drop triggers two reorder mutations. Since the mutation uses from/to indices, the second call can reorder a different workspace after the first update. Guard the end handler with `monitor.didDrop()` (as before) to avoid the duplicate mutation.</violation>
</file>

<file name="packages/local-db/drizzle/0035_add_workspace_sections.sql">

<violation number="1" location="packages/local-db/drizzle/0035_add_workspace_sections.sql:12">
P1: `section_id` should use `ON DELETE SET NULL` so deleting a section doesn’t fail with a FK constraint and instead returns workspaces to the ungrouped list.</violation>
</file>

<file name="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx:114">
P2: The reorder mutation fires twice on a normal section drop because both `drop` and `end` call `commitSectionReorder`. Guard `end` with `monitor.didDrop()` (or remove the drop commit) to avoid duplicate reorder requests.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

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

Comment thread packages/local-db/drizzle/0035_add_workspace_sections.sql Outdated
JimmyDore added a commit to JimmyDore/superset that referenced this pull request Mar 5, 2026
- Prevent duplicate reorder mutations via monitor.didDrop() guards
- Add projectId guard on cross-section workspace drops
- Null workspace sectionIds before deleting section (FK safety)
- Log warning instead of empty catch in ProjectHeader
- Add sectionId index on workspaces table
- Include section workspaces in keyboard shortcut flattening
- Clear auto-expand timer on section drop
- Extract shared invalidateWorkspaceQueries helper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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: 4

♻️ Duplicate comments (1)
apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts (1)

158-170: ⚠️ Potential issue | 🟠 Major

Validate workspace/section integrity before updating sectionId.

moveWorkspaceToSection currently writes blindly. It should verify: workspace exists, section exists (if non-null), and section.projectId matches workspace.projectId before update.

🛡️ Suggested fix
 		moveWorkspaceToSection: publicProcedure
 			.input(
 				z.object({
 					workspaceId: z.string(),
 					sectionId: z.string().nullable(),
 				}),
 			)
 			.mutation(({ input }) => {
+				const workspace = localDb
+					.select()
+					.from(workspaces)
+					.where(eq(workspaces.id, input.workspaceId))
+					.get();
+
+				if (!workspace) {
+					throw new Error(`Workspace ${input.workspaceId} not found`);
+				}
+
+				if (input.sectionId) {
+					const section = localDb
+						.select()
+						.from(workspaceSections)
+						.where(eq(workspaceSections.id, input.sectionId))
+						.get();
+
+					if (!section) {
+						throw new Error(`Section ${input.sectionId} not found`);
+					}
+
+					if (section.projectId !== workspace.projectId) {
+						throw new Error("Section belongs to a different project");
+					}
+				}
+
 				localDb
 					.update(workspaces)
 					.set({ sectionId: input.sectionId })
 					.where(eq(workspaces.id, input.workspaceId))
 					.run();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts` around
lines 158 - 170, In moveWorkspaceToSection, don’t write blindly: first query
localDb for the target workspace (use workspaces and the workspaceId input) and
throw/return an error if missing; if input.sectionId is non-null, query sections
for that id and error if missing; then verify the found section.projectId equals
the workspace.projectId and error if it doesn’t match; only after these checks
perform the localDb.update(workspaces).set({ sectionId: input.sectionId
}).where(eq(workspaces.id, input.workspaceId)).run(); ensure you reference the
same localDb, workspaces, and sections symbols so the checks and update operate
against the same datastore.
🧹 Nitpick comments (4)
apps/desktop/src/renderer/stores/active-drag-item.test.ts (1)

4-10: Consider typing the shared fixture to prevent contract drift.

testItem is currently inferred. Making it explicitly follow the store getter contract will catch schema drift at compile time.

♻️ Suggested refactor
 import { beforeEach, describe, expect, test } from "bun:test";
 import { getActiveDragItem, useActiveDragItemStore } from "./active-drag-item";
 
+type ActiveDragItem = NonNullable<ReturnType<typeof getActiveDragItem>>;
+
-const testItem = {
+const testItem: ActiveDragItem = {
 	id: "ws-1",
 	projectId: "p-1",
 	sectionId: null,
 	index: 0,
 	originalIndex: 0,
 };

As per coding guidelines, "Avoid any type unless necessary - prioritize type safety".

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

In `@apps/desktop/src/renderer/stores/active-drag-item.test.ts` around lines 4 -
10, The test fixture `testItem` should be explicitly typed to match the store's
expected item shape to prevent contract drift; import the appropriate type
(e.g., ActiveDragItem, DragItem, or the store getter's return type) from the
store/module and declare the fixture as `const testItem: <ImportedType> = { id:
"ws-1", projectId: "p-1", sectionId: null, index: 0, originalIndex: 0 };` so the
compiler enforces the schema rather than relying on inference.
apps/desktop/src/renderer/react-query/workspaces/invalidateWorkspaceQueries.ts (1)

6-9: Batch invalidations concurrently to reduce post-mutation latency.

Current behavior is correct, but these invalidations can run in parallel.

♻️ Suggested refactor
 export async function invalidateWorkspaceQueries(utils: Utils) {
-	await utils.workspaces.getAll.invalidate();
-	await utils.workspaces.getAllGrouped.invalidate();
-	await utils.workspaces.getPreviousWorkspace.invalidate();
-	await utils.workspaces.getNextWorkspace.invalidate();
+	await Promise.all([
+		utils.workspaces.getAll.invalidate(),
+		utils.workspaces.getAllGrouped.invalidate(),
+		utils.workspaces.getPreviousWorkspace.invalidate(),
+		utils.workspaces.getNextWorkspace.invalidate(),
+	]);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/react-query/workspaces/invalidateWorkspaceQueries.ts`
around lines 6 - 9, The four invalidate calls
(utils.workspaces.getAll.invalidate, utils.workspaces.getAllGrouped.invalidate,
utils.workspaces.getPreviousWorkspace.invalidate,
utils.workspaces.getNextWorkspace.invalidate) are awaited serially which
increases post-mutation latency; run them concurrently by replacing the
sequential awaits with a single await Promise.all([...]) (or return
Promise.all([...]) if appropriate) of those invalidate promises so all
invalidations execute in parallel.
apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts (1)

94-100: Wrap reorder persistence in a single DB transaction.

Both reorder mutations perform multi-row updates in loops; partial failures can leave inconsistent tabOrder states.

♻️ Suggested pattern
-				for (const section of sections) {
-					localDb
-						.update(workspaceSections)
-						.set({ tabOrder: section.tabOrder })
-						.where(eq(workspaceSections.id, section.id))
-						.run();
-				}
+				localDb.transaction(() => {
+					for (const section of sections) {
+						localDb
+							.update(workspaceSections)
+							.set({ tabOrder: section.tabOrder })
+							.where(eq(workspaceSections.id, section.id))
+							.run();
+					}
+				})();

Apply the same pattern to reorderWorkspacesInSection.

Also applies to: 147-153

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

In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts` around
lines 94 - 100, The multi-row updates in the loops that set tabOrder (currently
using localDb.update(workspaceSections).set(...).where(...).run() per section)
must be executed inside a single DB transaction to avoid partial failures;
update the code in the procedure(s) that perform these loops (the reorder
function that iterates over sections and the reorderWorkspacesInSection function
referenced in the comment) to start a transaction (e.g., localDb.transaction or
the project’s transaction API), perform all update calls against the
transactional object (not the top-level localDb), and commit/rollback together
so either all tabOrder updates succeed or none do. Ensure you replace the
per-iteration localDb.update(...) calls with transactional updates and await the
transaction completion.
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx (1)

549-588: Avoid no-op “Move to Section” mutations for current section.

Selecting the current section (or “No Section” when already unsectioned) still fires a mutation. Consider disabling or skipping these options to reduce unnecessary writes/invalidation.

♻️ Optional refactor
 							<ContextMenuItem
+								disabled={sectionId === null}
 								onSelect={() =>
+									sectionId === null
+										? undefined
+										:
 									moveToSection.mutate({
 										workspaceId: id,
 										sectionId: null,
 									})
 								}
 							>
 								No Section
 							</ContextMenuItem>
@@
 							{sections.map((section) => (
 								<ContextMenuItem
 									key={section.id}
+									disabled={section.id === sectionId}
 									onSelect={() =>
+										section.id === sectionId
+											? undefined
+											:
 										moveToSection.mutate({
 											workspaceId: id,
 											sectionId: section.id,
 										})
 									}
 								>
 									{section.name}
 								</ContextMenuItem>
 							))}
🤖 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/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx`
around lines 549 - 588, The context menu always calls moveToSection.mutate even
when selecting the workspace's current section (or "No Section"), causing
unnecessary mutations; update the onSelect handlers for the "No Section"
ContextMenuItem and the mapped ContextMenuItem (inside sections.map) to first
check current section equality (compare workspace id/section id via id and
section.id or null) and only call moveToSection.mutate when the target section
differs, and optionally disable the menu item by setting a disabled prop when
the target equals the current section to prevent no-op actions.
🤖 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/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx`:
- Around line 100-103: The mutation currently only toasts on error and must
instead rollback the optimistic reorder: in the mutation defined in
WorkspaceSection.tsx (the one with onError: toast and the optimistic update done
in hover/onMutate), capture and return the previous sections list from onMutate
(e.g., return { previousSections }) and then in onError use that context to
restore the cache via queryClient.setQueryData (or equivalent) to
previousSections, then show the toast; also optionally call
queryClient.invalidateQueries for the sections query to ensure server state
sync.
- Around line 115-119: The end() handler currently calls
commitSectionReorder(item) when monitor.didDrop() is false, which persists an
optimistic reorder that should be reverted; change end() so that if
!monitor.didDrop() you do NOT call commitSectionReorder but instead invalidate
the sections cache to roll back the optimistic change (use the same
react-query/query client and query key used by your sections fetcher — e.g.,
call queryClient.invalidateQueries(<the sections query key> or the hook's query
key like useSectionsQuery key) ); keep hover() for optimistic updates and leave
drop() to call commitSectionReorder when the drop is accepted.

In `@packages/local-db/src/schema/schema.ts`:
- Around line 154-159: Add a unique constraint/index to protect the per-project
ordering by making (project_id, tab_order) unique in the table definition where
projectId and tabOrder are declared (the section schema that references
projects.id and defines tabOrder and projectId); update the table schema to add
a unique index/constraint on these two columns (and apply the same change to the
other identical section definition mentioned) so concurrent inserts cannot
create duplicate tab_order values for the same project.
- Around line 124-126: Add a DB-level guard ensuring workspaces.section_id
(non-null) belongs to the same project as workspaces.project_id by creating a
trigger function and trigger on the workspaces table: implement a BEFORE INSERT
OR UPDATE trigger (e.g., enforce_workspace_section_same_project) that, when
NEW.section_id is not null, SELECTs project_id FROM workspace_sections WHERE id
= NEW.section_id and compares it to NEW.project_id and raises an exception if
they differ; attach this trigger to the workspaces table (update the
schema/migration where sectionId is defined) so any insert/update violating the
same-project invariant is rejected at the DB level.

---

Duplicate comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts`:
- Around line 158-170: In moveWorkspaceToSection, don’t write blindly: first
query localDb for the target workspace (use workspaces and the workspaceId
input) and throw/return an error if missing; if input.sectionId is non-null,
query sections for that id and error if missing; then verify the found
section.projectId equals the workspace.projectId and error if it doesn’t match;
only after these checks perform the localDb.update(workspaces).set({ sectionId:
input.sectionId }).where(eq(workspaces.id, input.workspaceId)).run(); ensure you
reference the same localDb, workspaces, and sections symbols so the checks and
update operate against the same datastore.

---

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts`:
- Around line 94-100: The multi-row updates in the loops that set tabOrder
(currently using localDb.update(workspaceSections).set(...).where(...).run() per
section) must be executed inside a single DB transaction to avoid partial
failures; update the code in the procedure(s) that perform these loops (the
reorder function that iterates over sections and the reorderWorkspacesInSection
function referenced in the comment) to start a transaction (e.g.,
localDb.transaction or the project’s transaction API), perform all update calls
against the transactional object (not the top-level localDb), and
commit/rollback together so either all tabOrder updates succeed or none do.
Ensure you replace the per-iteration localDb.update(...) calls with
transactional updates and await the transaction completion.

In
`@apps/desktop/src/renderer/react-query/workspaces/invalidateWorkspaceQueries.ts`:
- Around line 6-9: The four invalidate calls
(utils.workspaces.getAll.invalidate, utils.workspaces.getAllGrouped.invalidate,
utils.workspaces.getPreviousWorkspace.invalidate,
utils.workspaces.getNextWorkspace.invalidate) are awaited serially which
increases post-mutation latency; run them concurrently by replacing the
sequential awaits with a single await Promise.all([...]) (or return
Promise.all([...]) if appropriate) of those invalidate promises so all
invalidations execute in parallel.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx`:
- Around line 549-588: The context menu always calls moveToSection.mutate even
when selecting the workspace's current section (or "No Section"), causing
unnecessary mutations; update the onSelect handlers for the "No Section"
ContextMenuItem and the mapped ContextMenuItem (inside sections.map) to first
check current section equality (compare workspace id/section id via id and
section.id or null) and only call moveToSection.mutate when the target section
differs, and optionally disable the menu item by setting a disabled prop when
the target equals the current section to prevent no-op actions.

In `@apps/desktop/src/renderer/stores/active-drag-item.test.ts`:
- Around line 4-10: The test fixture `testItem` should be explicitly typed to
match the store's expected item shape to prevent contract drift; import the
appropriate type (e.g., ActiveDragItem, DragItem, or the store getter's return
type) from the store/module and declare the fixture as `const testItem:
<ImportedType> = { id: "ws-1", projectId: "p-1", sectionId: null, index: 0,
originalIndex: 0 };` so the compiler enforces the schema rather than relying on
inference.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d381f589-ab16-4373-86eb-93892d1d65a1

📥 Commits

Reviewing files that changed from the base of the PR and between 59513be and a61ef82.

📒 Files selected for processing (18)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts
  • apps/desktop/src/renderer/hooks/useWorkspaceShortcuts.ts
  • apps/desktop/src/renderer/react-query/workspaces/invalidateWorkspaceQueries.ts
  • apps/desktop/src/renderer/react-query/workspaces/useMoveWorkspaceToSection.ts
  • apps/desktop/src/renderer/react-query/workspaces/useReorderSections.ts
  • apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts
  • apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspacesInSection.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/hooks/useSectionDropZone.ts
  • apps/desktop/src/renderer/stores/active-drag-item.test.ts
  • packages/local-db/drizzle/0036_add_workspaces_section_id_idx.sql
  • packages/local-db/drizzle/meta/0036_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/src/schema/schema.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/local-db/drizzle/meta/0036_snapshot.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/local-db/drizzle/meta/_journal.json
  • apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspacesInSection.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/hooks/useSectionDropZone.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts

Comment thread packages/local-db/src/schema/schema.ts
Comment thread packages/local-db/src/schema/schema.ts
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.

1 issue found across 37 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/local-db/drizzle/0035_add_workspace_sections.sql">

<violation number="1" location="packages/local-db/drizzle/0035_add_workspace_sections.sql:12">
P2: The new workspaces.section_id FK is missing `ON DELETE SET NULL`, so deleting a section can fail or leave orphaned references. Add the delete action so workspaces revert to ungrouped as intended.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

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

Comment thread packages/local-db/drizzle/0035_add_workspace_sections.sql Outdated
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.

♻️ Duplicate comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx (1)

117-121: ⚠️ Potential issue | 🟠 Major

Do not persist reorder on canceled drag.

At Line 119-Line 120, end() commits reorder when monitor.didDrop() is false, which can persist a hover-only optimistic order after a canceled drop. Revert cache instead; keep persistence in drop() only.

Suggested fix
 			end: (item, monitor) => {
 				if (!item) return;
-				if (monitor.didDrop()) return;
-				commitSectionReorder(item);
+				if (!monitor.didDrop()) {
+					void utils.workspaces.getAllGrouped.invalidate();
+				}
 			},
react-dnd 16 useDrag end callback monitor.didDrop behavior canceled drop versus accepted drop
🤖 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/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx`
around lines 117 - 121, The end() drag handler currently calls
commitSectionReorder(item) when monitor.didDrop() is false, which persists an
optimistic hover reorder after a canceled drop; change end() to only call
commitSectionReorder when monitor.didDrop() is true and otherwise revert the
optimistic cache/state instead of persisting (i.e., undo the hover reorder via
the existing cache reset or revert function used elsewhere), and ensure the
actual persistence remains in the drop() handler so commits happen only on
accepted drops.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx`:
- Around line 117-121: The end() drag handler currently calls
commitSectionReorder(item) when monitor.didDrop() is false, which persists an
optimistic hover reorder after a canceled drop; change end() to only call
commitSectionReorder when monitor.didDrop() is true and otherwise revert the
optimistic cache/state instead of persisting (i.e., undo the hover reorder via
the existing cache reset or revert function used elsewhere), and ensure the
actual persistence remains in the drop() handler so commits happen only on
accepted drops.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99991222-c8a4-4015-b969-1d96369f23eb

📥 Commits

Reviewing files that changed from the base of the PR and between a61ef82 and ca9b263.

📒 Files selected for processing (3)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx
  • packages/local-db/drizzle/0035_add_workspace_sections.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts

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.

2 issues found across 37 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="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx:219">
P2: Double-click-to-rename is coupled with click-to-toggle on the same button, so rename attempts also send collapse toggle mutations.</violation>
</file>

<file name="apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts">

<violation number="1" location="apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts:51">
P2: `computeVisualOrder` can silently omit workspaces when `sectionId` is non-null but unresolved within the project, causing previous/next workspace navigation to skip those items. Add the same orphan fallback used in `getAllGrouped` (treat unresolved sections as ungrouped).</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

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

Comment thread apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts
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.

♻️ Duplicate comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx (1)

117-121: ⚠️ Potential issue | 🟠 Major

Do not persist reorder when drag ends without a valid drop.

Line 120 commits reorder when monitor.didDrop() is false, which represents a canceled/unhandled drop in react-dnd. That can persist hover-only optimistic state. Revert cache on canceled drag and keep commit only in accepted drop flow.

Proposed fix
 			end: (item, monitor) => {
 				if (!item) return;
-				if (monitor.didDrop()) return;
-				commitSectionReorder(item);
+				if (!monitor.didDrop()) {
+					void utils.workspaces.getAllGrouped.invalidate();
+				}
 			},
@@
 		drop: (item: SectionDragItem) => {
+			if (item.projectId !== projectId) return;
 			commitSectionReorder(item);
 			if (item.originalIndex !== item.index) return { reordered: true };
 		},
In react-dnd v16, does source end() always fire, and does monitor.didDrop() return false when no compatible drop target handled the drop?

Also applies to: 143-145

🤖 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/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx`
around lines 117 - 121, The end handler currently calls commitSectionReorder
even when monitor.didDrop() is false, which persists optimistic hover-only
state; update the end: (item, monitor) => { ... } handler to only call
commitSectionReorder when monitor.didDrop() === true and, when monitor.didDrop()
=== false, revert the optimistic reorder (e.g., restore the previous sections
order via the same state-reset/revert helper used for optimistic updates or add
a new revertSectionOrder function) so canceled/unhandled drops do not persist;
apply the same change to the other drag end handler that mirrors this logic (the
similar end handler at the later block).
🧹 Nitpick comments (2)
apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts (1)

54-57: Consider replacing repeated per-section filtering with a pre-grouped map.

This loop rescans projectWorkspaces for every section. Pre-grouping once keeps behavior identical and reduces repeated allocations/scans.

♻️ Suggested refactor
 		const sectionIds = new Set(projectSections.map((s) => s.id));
+		const workspacesBySection = new Map<string | null, WorkspaceLike[]>();

-		// Ungrouped workspaces: null sectionId OR orphaned (sectionId not in project)
-		for (const ws of projectWorkspaces.filter(
-			(w) => w.sectionId === null || !sectionIds.has(w.sectionId),
-		)) {
-			orderedIds.push(ws.id);
-		}
+		for (const ws of projectWorkspaces) {
+			const key =
+				ws.sectionId !== null && sectionIds.has(ws.sectionId)
+					? ws.sectionId
+					: null;
+			const bucket = workspacesBySection.get(key);
+			if (bucket) {
+				bucket.push(ws);
+			} else {
+				workspacesBySection.set(key, [ws]);
+			}
+		}
+
+		// Ungrouped workspaces: null sectionId OR orphaned (sectionId not in project)
+		for (const ws of workspacesBySection.get(null) ?? []) {
+			orderedIds.push(ws.id);
+		}

 		for (const section of projectSections) {
-			for (const ws of projectWorkspaces.filter(
-				(w) => w.sectionId === section.id,
-			)) {
+			for (const ws of workspacesBySection.get(section.id) ?? []) {
 				orderedIds.push(ws.id);
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts` around
lines 54 - 57, The loop over projectSections repeatedly filters
projectWorkspaces for each section causing redundant scans; pre-group
projectWorkspaces once into a map keyed by sectionId (e.g., build
groupedWorkspacesBySectionId from projectWorkspaces) and then iterate
projectSections and lookup workspaces via
groupedWorkspacesBySectionId.get(section.id) (falling back to an empty array) to
preserve behavior while avoiding repeated allocations and filtering in the
nested for-loops that reference projectSections and projectWorkspaces.
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx (1)

11-11: Clear the click timer on unmount.

The pending timeout can still fire after unmount and dispatch toggleCollapsed.mutate. Add cleanup to clear clickTimer.

Proposed fix
-import { useCallback, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
@@
 	const clickTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+	useEffect(() => {
+		return () => {
+			if (clickTimer.current) {
+				clearTimeout(clickTimer.current);
+				clickTimer.current = null;
+			}
+		};
+	}, []);

Also applies to: 149-157

🤖 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/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx`
at line 11, The component registers a pending timeout via clickTimer that can
fire after unmount and call toggleCollapsed.mutate; add cleanup to clear that
timer on unmount by using a useEffect cleanup that calls clearTimeout on the ref
(e.g., clearTimeout(clickTimer.current)); also ensure you clear any existing
timer before setting a new one in the click handler to avoid duplicates. Apply
the same cleanup/fix for the other instance mentioned around lines 149-157 so
both clickTimer usages are cleared on unmount and before resetting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx`:
- Around line 117-121: The end handler currently calls commitSectionReorder even
when monitor.didDrop() is false, which persists optimistic hover-only state;
update the end: (item, monitor) => { ... } handler to only call
commitSectionReorder when monitor.didDrop() === true and, when monitor.didDrop()
=== false, revert the optimistic reorder (e.g., restore the previous sections
order via the same state-reset/revert helper used for optimistic updates or add
a new revertSectionOrder function) so canceled/unhandled drops do not persist;
apply the same change to the other drag end handler that mirrors this logic (the
similar end handler at the later block).

---

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts`:
- Around line 54-57: The loop over projectSections repeatedly filters
projectWorkspaces for each section causing redundant scans; pre-group
projectWorkspaces once into a map keyed by sectionId (e.g., build
groupedWorkspacesBySectionId from projectWorkspaces) and then iterate
projectSections and lookup workspaces via
groupedWorkspacesBySectionId.get(section.id) (falling back to an empty array) to
preserve behavior while avoiding repeated allocations and filtering in the
nested for-loops that reference projectSections and projectWorkspaces.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx`:
- Line 11: The component registers a pending timeout via clickTimer that can
fire after unmount and call toggleCollapsed.mutate; add cleanup to clear that
timer on unmount by using a useEffect cleanup that calls clearTimeout on the ref
(e.g., clearTimeout(clickTimer.current)); also ensure you clear any existing
timer before setting a new one in the click handler to avoid duplicates. Apply
the same cleanup/fix for the other instance mentioned around lines 149-157 so
both clickTimer usages are cleared on unmount and before resetting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d7b2dc8-4cdf-4739-95ef-55897163fe46

📥 Commits

Reviewing files that changed from the base of the PR and between ca9b263 and aa68a9c.

📒 Files selected for processing (2)
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/visual-order.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx

JimmyDore and others added 8 commits March 6, 2026 07:48
Add the ability to organize workspaces into custom sections within the
sidebar. Users can create, rename, and delete sections, drag workspaces
between sections, reorder workspaces within a section, and reorder
sections themselves.

Changes:
- Add workspace_sections table and sectionId column to local DB schema
- Add tRPC procedures for workspace sections CRUD operations
- Include sections in getAllGrouped query response
- Add WorkspaceSection, WorkspaceList UI components
- Add 'Move to Section' context menu on workspace items
- Add drag & drop support for workspaces between sections
- Add intra-section workspace reordering and section DnD
- Extract useSectionDropZone hook and active-drag-item store
Extract pure utility functions (reorderItems, computeNextTabOrder,
computeVisualOrder) from tRPC procedures and test them directly.
Add Zustand store tests for active-drag-item.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Prevent duplicate reorder mutations via monitor.didDrop() guards
- Add projectId guard on cross-section workspace drops
- Null workspace sectionIds before deleting section (FK safety)
- Log warning instead of empty catch in ProjectHeader
- Add sectionId index on workspaces table
- Include section workspaces in keyboard shortcut flattening
- Clear auto-expand timer on section drop
- Extract shared invalidateWorkspaceQueries helper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…order, fix migration FK

- Add workspace/section existence and cross-project validation to moveWorkspaceToSection
- Invalidate getAllGrouped on section reorder error to rollback stale optimistic UI
- Add ON DELETE SET NULL to section_id FK in migration SQL to match Drizzle schema

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ectionIds

- Debounce single click on section header so double-click-to-rename doesn't
  also fire the collapse toggle
- Treat workspaces with orphaned sectionId (pointing to non-existent section)
  as ungrouped in computeVisualOrder to prevent them from being silently omitted

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ions

Cmd+click toggles individual workspace selection, Shift+click selects a
contiguous range. Dragging a selected group onto a section moves all
selected workspaces. Right-click "Move to Section" also bulk-moves when
multiple are selected.

- Zustand selection store (workspace-selection.ts)
- Bulk moveWorkspacesToSection tRPC procedure + React Query hook
- MultiDragPreview floating badge during multi-drag
- Selection visual highlight and opacity on drag
- Escape / click-empty-space clears selection
- Snapshot selection on context menu open for reliable bulk moves

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…op handler

The native HTML5 drop zone wasn't reliably firing when dropping onto a
workspace item in a different section. Now the react-dnd useDrop handler
directly handles cross-section moves (both single and multi-select),
with an item.handled guard to prevent double-processing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Batch DB writes with inArray instead of N+1 loop, optimize MultiDragPreview
collector to skip work when not dragging, remove dead projectId prop from
WorkspaceList, use store actions instead of raw setState, guard Escape key
handler against input elements, and remove unused getSelectedWorkspaceIds export.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@JimmyDore JimmyDore force-pushed the jimmy-dore/featuresidebar-ordering branch from d994b82 to 9defc7c Compare March 6, 2026 06:50
@JimmyDore
Copy link
Copy Markdown
Contributor Author

JimmyDore commented Mar 6, 2026

@Kitenite PR ready to review

  • First (big) contribution to OpenSource repo, there was a lot of back and forth, I tried to simplify code as much as possible, let me know all your comments and I'll be happy to fix
  • I did not find where to add you as a reviewer
  • I think code rabbit did not run on the last multi select commits, it paused, I tried to resume but does not seem to work 🤔

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.

2 issues found across 14 files (changes from recent commits).

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="apps/desktop/src/renderer/react-query/workspaces/useCreateSectionFromWorkspaces.ts">

<violation number="1" location="apps/desktop/src/renderer/react-query/workspaces/useCreateSectionFromWorkspaces.ts:29">
P2: If `moveWorkspaces` fails after the section is successfully created, `invalidateWorkspaceQueries` is skipped, leaving the UI out of sync with the database. Move the invalidation call to a `finally` block so it runs regardless of whether the second mutation succeeds.</violation>
</file>

<file name="packages/local-db/drizzle/0035_add_workspace_sections.sql">

<violation number="1" location="packages/local-db/drizzle/0035_add_workspace_sections.sql:14">
P1: Missing `ON DELETE SET NULL` on the `section_id` foreign key. The Drizzle schema specifies `onDelete: "set null"` so that deleting a section returns its workspaces to the ungrouped state, but this migration drops that clause. With the default `NO ACTION`, deleting a section will either fail (if `PRAGMA foreign_keys` is on) or leave dangling `section_id` references (if it's off).</violation>
</file>

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

--> statement-breakpoint
CREATE INDEX `workspace_sections_project_id_idx` ON `workspace_sections` (`project_id`);--> statement-breakpoint
ALTER TABLE `workspaces` ADD `section_id` text REFERENCES workspace_sections(id);--> statement-breakpoint
CREATE INDEX `workspaces_section_id_idx` ON `workspaces` (`section_id`); No newline at end of file
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 7, 2026

Choose a reason for hiding this comment

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

P1: Missing ON DELETE SET NULL on the section_id foreign key. The Drizzle schema specifies onDelete: "set null" so that deleting a section returns its workspaces to the ungrouped state, but this migration drops that clause. With the default NO ACTION, deleting a section will either fail (if PRAGMA foreign_keys is on) or leave dangling section_id references (if it's off).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/local-db/drizzle/0035_add_workspace_sections.sql, line 14:

<comment>Missing `ON DELETE SET NULL` on the `section_id` foreign key. The Drizzle schema specifies `onDelete: "set null"` so that deleting a section returns its workspaces to the ungrouped state, but this migration drops that clause. With the default `NO ACTION`, deleting a section will either fail (if `PRAGMA foreign_keys` is on) or leave dangling `section_id` references (if it's off).</comment>

<file context>
@@ -4,9 +4,11 @@ CREATE TABLE `workspace_sections` (
-ALTER TABLE `workspaces` ADD `section_id` text REFERENCES workspace_sections(id) ON DELETE SET NULL;
\ No newline at end of file
+ALTER TABLE `workspaces` ADD `section_id` text REFERENCES workspace_sections(id);--> statement-breakpoint
+CREATE INDEX `workspaces_section_id_idx` ON `workspaces` (`section_id`);
\ No newline at end of file
</file context>
Fix with Cubic

workspaceIds,
sectionId: section.id,
});
await invalidateWorkspaceQueries(utils);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 7, 2026

Choose a reason for hiding this comment

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

P2: If moveWorkspaces fails after the section is successfully created, invalidateWorkspaceQueries is skipped, leaving the UI out of sync with the database. Move the invalidation call to a finally block so it runs regardless of whether the second mutation succeeds.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/react-query/workspaces/useCreateSectionFromWorkspaces.ts, line 29:

<comment>If `moveWorkspaces` fails after the section is successfully created, `invalidateWorkspaceQueries` is skipped, leaving the UI out of sync with the database. Move the invalidation call to a `finally` block so it runs regardless of whether the second mutation succeeds.</comment>

<file context>
@@ -0,0 +1,38 @@
+				workspaceIds,
+				sectionId: section.id,
+			});
+			await invalidateWorkspaceQueries(utils);
+		} catch (error) {
+			toast.error(
</file context>
Fix with Cubic

Kitenite added 3 commits March 6, 2026 16:28
…on, typecheck fix

- Add color column to workspace sections with random color on creation
- Add colored dot and left border to section UI
- Add color picker context menu for sections
- Add "Create Section from Selection" on multi-select right-click
- Fix pre-existing typecheck error in ChangesView OrderedSection union type
- Consolidate migrations into single 0035_add_workspace_sections
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.

1 issue found across 2 files (changes from recent commits).

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="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx:262">
P1: The `min-h-8` class is gated on `isDragOver`, but `isDragOver` requires the user to hover over the element — which is effectively impossible when the div has zero height (no workspaces). The original code always applied `minHeight: "2rem"` on empty, ensuring the drop target was always reachable. This change makes it impossible to drag a workspace back to the ungrouped area when it's empty.</violation>
</file>

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

Kitenite added 3 commits March 6, 2026 16:46
… show drop targets during drag

- Extract useWorkspaceDnD hook from WorkspaceListItem (DnD setup, reorder, move-to-section on drop)
- Extract WorkspaceContextMenu component (context menu items, hover card wrapping)
- Extract useSectionMutations hook from WorkspaceSection (toggle/rename/delete/setColor)
- Add onSettled invalidation to reorder callbacks to restore server state on failure
- Move WORKSPACE_DND_TYPE to constants.ts
- Show drop zone indicators when any compatible item is dragging (not just on hover)
- useSectionDropZone now returns isDropTarget (any valid drag active) alongside isDragOver
@Kitenite Kitenite merged commit 4e35e22 into superset-sh:main Mar 7, 2026
13 of 15 checks passed
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.

4 issues found across 9 files (changes from recent commits).

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="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx:142">
P2: `useCallback` with `[mutations]` dependency never memoizes because `useSectionMutations` returns a new object on every render (no `useMemo`). Either memoize the return value in `useSectionMutations` with `useMemo`, or depend on a stable reference like `mutations.toggle` directly.</violation>

<violation number="2" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx:203">
P2: Switching from `ring-1` to `border` introduces layout shift — the 1px border appears/disappears during drag, pushing content inward. Use `ring-1` (box-shadow based, no layout effect) instead, or add a permanent `border border-transparent` to the base classes so only the color/style changes on drag.</violation>
</file>

<file name="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/useWorkspaceDnD.ts">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/useWorkspaceDnD.ts:106">
P2: `useDrop` should use the factory function + deps pattern (`useDrop(() => spec, deps)`) to match `useDrag` and avoid tearing down/re-registering the drop target on every render. This matters in a list where each item creates its own drop target.</violation>
</file>

<file name="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx">

<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx:267">
P2: Missing `border` (border-width) class when `isDragOver` is true. `border-solid` and `border-primary/30` have no visible effect without `border` because Tailwind defaults border-width to 0. The drag-over border indicator won't render.</violation>
</file>

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

clickTimer.current = null;
mutations.toggle();
}, 250);
}, [mutations]);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 7, 2026

Choose a reason for hiding this comment

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

P2: useCallback with [mutations] dependency never memoizes because useSectionMutations returns a new object on every render (no useMemo). Either memoize the return value in useSectionMutations with useMemo, or depend on a stable reference like mutations.toggle directly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx, line 142:

<comment>`useCallback` with `[mutations]` dependency never memoizes because `useSectionMutations` returns a new object on every render (no `useMemo`). Either memoize the return value in `useSectionMutations` with `useMemo`, or depend on a stable reference like `mutations.toggle` directly.</comment>

<file context>
@@ -181,9 +137,9 @@ export function WorkspaceSection({
+			mutations.toggle();
 		}, 250);
-	}, [sectionId, toggleCollapsed]);
+	}, [mutations]);
 
 	const handleDoubleClick = useCallback(() => {
</file context>
Fix with Cubic

className={cn(
"flex items-center w-full pl-2 pr-2 py-2 text-[11px] font-medium uppercase tracking-wider",
"text-muted-foreground hover:bg-muted/50 transition-colors",
dropZone.isDropTarget &&
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 7, 2026

Choose a reason for hiding this comment

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

P2: Switching from ring-1 to border introduces layout shift — the 1px border appears/disappears during drag, pushing content inward. Use ring-1 (box-shadow based, no layout effect) instead, or add a permanent border border-transparent to the base classes so only the color/style changes on drag.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSection/WorkspaceSection.tsx, line 203:

<comment>Switching from `ring-1` to `border` introduces layout shift — the 1px border appears/disappears during drag, pushing content inward. Use `ring-1` (box-shadow based, no layout effect) instead, or add a permanent `border border-transparent` to the base classes so only the color/style changes on drag.</comment>

<file context>
@@ -248,7 +200,11 @@ export function WorkspaceSection({
 							"flex items-center w-full pl-2 pr-2 py-2 text-[11px] font-medium uppercase tracking-wider",
 							"text-muted-foreground hover:bg-muted/50 transition-colors",
-							dropZone.isDragOver && "bg-primary/10 ring-1 ring-primary/40",
+							dropZone.isDropTarget &&
+								!dropZone.isDragOver &&
+								"border border-dashed border-primary/20 rounded-sm",
</file context>
Fix with Cubic

[id, projectId, sectionId, index, handleReorder],
);

const [, drop] = useDrop({
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 7, 2026

Choose a reason for hiding this comment

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

P2: useDrop should use the factory function + deps pattern (useDrop(() => spec, deps)) to match useDrag and avoid tearing down/re-registering the drop target on every render. This matters in a list where each item creates its own drop target.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/useWorkspaceDnD.ts, line 106:

<comment>`useDrop` should use the factory function + deps pattern (`useDrop(() => spec, deps)`) to match `useDrag` and avoid tearing down/re-registering the drop target on every render. This matters in a list where each item creates its own drop target.</comment>

<file context>
@@ -0,0 +1,162 @@
+		[id, projectId, sectionId, index, handleReorder],
+	);
+
+	const [, drop] = useDrop({
+		accept: WORKSPACE_DND_TYPE,
+		hover: (item: DragItem) => {
</file context>
Fix with Cubic

Comment on lines +267 to +268
ungroupedDropZone.isDragOver &&
"bg-primary/5 border-solid border-primary/30",
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 7, 2026

Choose a reason for hiding this comment

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

P2: Missing border (border-width) class when isDragOver is true. border-solid and border-primary/30 have no visible effect without border because Tailwind defaults border-width to 0. The drag-over border indicator won't render.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx, line 267:

<comment>Missing `border` (border-width) class when `isDragOver` is true. `border-solid` and `border-primary/30` have no visible effect without `border` because Tailwind defaults border-width to 0. The drag-over border indicator won't render.</comment>

<file context>
@@ -256,12 +256,16 @@ export function ProjectSection({
+										ungroupedDropZone.isDragOver) &&
 										workspaces.length === 0 &&
 										"min-h-8",
+									ungroupedDropZone.isDragOver &&
+										"bg-primary/5 border-solid border-primary/30",
 								)}
</file context>
Suggested change
ungroupedDropZone.isDragOver &&
"bg-primary/5 border-solid border-primary/30",
ungroupedDropZone.isDragOver &&
"bg-primary/5 border border-solid border-primary/30",
Fix with Cubic

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.

2 participants