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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -97,33 +97,35 @@ export function DashboardSidebarWorkspaceContextMenu({
</>
)}
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>
<LuArrowRightLeft className="size-4 mr-2" />
Move to Section
</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem onSelect={onCreateSection}>
<LuFolderPlus className="size-4 mr-2" />
New Section
</ContextMenuItem>
{sections.length > 0 && <ContextMenuSeparator />}
{sections.map((section) => (
<ContextMenuItem
key={section.id}
onSelect={() => onMoveToSection(section.id)}
>
{section.color && (
<span
className="size-2 shrink-0 rounded-full mr-2"
style={{ backgroundColor: section.color }}
/>
)}
{section.name}
</ContextMenuItem>
))}
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem onSelect={onCreateSection}>
<LuFolderPlus className="size-4 mr-2" />
Create Section Below
</ContextMenuItem>
{(sections.length > 0 || isInSection) && <ContextMenuSeparator />}
{sections.length > 0 && (
<ContextMenuSub>
<ContextMenuSubTrigger>
<LuArrowRightLeft className="size-4 mr-2" />
Move to Section
</ContextMenuSubTrigger>
<ContextMenuSubContent>
{sections.map((section) => (
<ContextMenuItem
key={section.id}
onSelect={() => onMoveToSection(section.id)}
>
{section.color && (
<span
className="size-2 shrink-0 rounded-full mr-2"
style={{ backgroundColor: section.color }}
/>
)}
{section.name}
</ContextMenuItem>
))}
</ContextMenuSubContent>
</ContextMenuSub>
)}
{isInSection && (
<ContextMenuItem onSelect={() => onMoveToSection(null)}>
<LuArrowUp className="size-4 mr-2" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ export function useDashboardSidebarWorkspaceItemActions({
};

const handleCreateSection = () => {
const newSectionId = createSection(projectId);
moveWorkspaceToSection(workspaceId, projectId, newSectionId);
createSection(projectId, {
insertAfterWorkspaceId: workspaceId,
});
};

const resolveWorktreePath = async (): Promise<string | null> => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,25 +182,71 @@ export function useDashboardSidebarState() {
);

const createSection = useCallback(
(projectId: string, name = "New Section") => {
(
projectId: string,
options: { name?: string; insertAfterWorkspaceId?: string } = {},
) => {
const { name = "New Section", insertAfterWorkspaceId } = options;
ensureSidebarProjectRecord(collections, projectId);

const sectionId = crypto.randomUUID();
const sectionOrders = Array.from(
collections.v2SidebarSections.state.values(),
).filter((item) => item.projectId === projectId);

const randomColor =
PROJECT_CUSTOM_COLORS[
Math.floor(Math.random() * PROJECT_CUSTOM_COLORS.length)
].value;

let tabOrder: number;
if (insertAfterWorkspaceId) {
const anchorWorkspace = collections.v2WorkspaceLocalState.get(
insertAfterWorkspaceId,
);
Comment on lines +199 to +202
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P2: When insertAfterWorkspaceId doesn't resolve to a workspace (stale ID or race condition), anchorWorkspace is undefined and anchorTabOrder falls back to 0. This causes every top-level item with tabOrder > 0 to be bumped and the new section to land at the top of the project — silently corrupting the sidebar order.

Guard against this by checking that anchorWorkspace actually resolved before entering the bump logic, and fall through to the append-at-end branch otherwise:

const anchorWorkspace = insertAfterWorkspaceId
  ? collections.v2WorkspaceLocalState.get(insertAfterWorkspaceId)
  : undefined;
if (anchorWorkspace && anchorWorkspace.sidebarState.projectId === projectId) {
  // ... bump logic with anchorTabOrder
} else {
  // append-at-end
}
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts, line 199:

<comment>When `insertAfterWorkspaceId` doesn't resolve to a workspace (stale ID or race condition), `anchorWorkspace` is `undefined` and `anchorTabOrder` falls back to `0`. This causes every top-level item with `tabOrder > 0` to be bumped and the new section to land at the top of the project — silently corrupting the sidebar order.

Guard against this by checking that `anchorWorkspace` actually resolved before entering the bump logic, and fall through to the append-at-end branch otherwise:

```ts
const anchorWorkspace = insertAfterWorkspaceId
  ? collections.v2WorkspaceLocalState.get(insertAfterWorkspaceId)
  : undefined;
if (anchorWorkspace && anchorWorkspace.sidebarState.projectId === projectId) {
  // ... bump logic with anchorTabOrder
} else {
  // append-at-end
}
```</comment>

<file context>
@@ -182,25 +182,71 @@ export function useDashboardSidebarState() {
 				].value;
 
+			let tabOrder: number;
+			if (insertAfterWorkspaceId) {
+				const anchorWorkspace = collections.v2WorkspaceLocalState.get(
+					insertAfterWorkspaceId,
</file context>
Suggested change
if (insertAfterWorkspaceId) {
const anchorWorkspace = collections.v2WorkspaceLocalState.get(
insertAfterWorkspaceId,
);
const anchorWorkspace = insertAfterWorkspaceId
? collections.v2WorkspaceLocalState.get(insertAfterWorkspaceId)
: undefined;
if (anchorWorkspace && anchorWorkspace.sidebarState.projectId === projectId) {
Fix with Cubic

const anchorTabOrder = anchorWorkspace?.sidebarState.sectionId
? (collections.v2SidebarSections.get(
anchorWorkspace.sidebarState.sectionId,
)?.tabOrder ?? 0)
: (anchorWorkspace?.sidebarState.tabOrder ?? 0);
Comment on lines +199 to +207
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Missing guard for unknown anchor workspace

If insertAfterWorkspaceId is provided but the workspace is not found in v2WorkspaceLocalState (e.g., stale state, timing edge case), anchorWorkspace is undefined. Both optional chains resolve to undefined, so anchorTabOrder silently defaults to 0. This causes every top-level workspace and section in the project to be bumped by +1 and the new section to land at tabOrder = 1 (the very top of the sidebar) — the opposite of the user's intent.

Consider adding a guard to fall back to the append behavior when the anchor cannot be resolved:

if (insertAfterWorkspaceId) {
    const anchorWorkspace = collections.v2WorkspaceLocalState.get(
        insertAfterWorkspaceId,
    );
    if (!anchorWorkspace) {
        // Anchor not found – fall back to append
        const sectionOrders = Array.from(
            collections.v2SidebarSections.state.values(),
        ).filter((item) => item.projectId === projectId);
        tabOrder = getNextTabOrder(sectionOrders);
    } else {
        const anchorTabOrder = anchorWorkspace.sidebarState.sectionId
            ? (collections.v2SidebarSections.get(
                    anchorWorkspace.sidebarState.sectionId,
                )?.tabOrder ?? 0)
            : anchorWorkspace.sidebarState.tabOrder;
        // ... bump loop and tabOrder assignment ...
    }
}

This also applies if the section pointed to by sidebarState.sectionId has been deleted — the ?.tabOrder ?? 0 fallback inside the ternary has the same problem.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts
Line: 199-207

Comment:
**Missing guard for unknown anchor workspace**

If `insertAfterWorkspaceId` is provided but the workspace is not found in `v2WorkspaceLocalState` (e.g., stale state, timing edge case), `anchorWorkspace` is `undefined`. Both optional chains resolve to `undefined`, so `anchorTabOrder` silently defaults to `0`. This causes every top-level workspace and section in the project to be bumped by +1 and the new section to land at `tabOrder = 1` (the very top of the sidebar) — the opposite of the user's intent.

Consider adding a guard to fall back to the append behavior when the anchor cannot be resolved:

```ts
if (insertAfterWorkspaceId) {
    const anchorWorkspace = collections.v2WorkspaceLocalState.get(
        insertAfterWorkspaceId,
    );
    if (!anchorWorkspace) {
        // Anchor not found – fall back to append
        const sectionOrders = Array.from(
            collections.v2SidebarSections.state.values(),
        ).filter((item) => item.projectId === projectId);
        tabOrder = getNextTabOrder(sectionOrders);
    } else {
        const anchorTabOrder = anchorWorkspace.sidebarState.sectionId
            ? (collections.v2SidebarSections.get(
                    anchorWorkspace.sidebarState.sectionId,
                )?.tabOrder ?? 0)
            : anchorWorkspace.sidebarState.tabOrder;
        // ... bump loop and tabOrder assignment ...
    }
}
```

This also applies if the section pointed to by `sidebarState.sectionId` has been deleted — the `?.tabOrder ?? 0` fallback inside the ternary has the same problem.

How can I resolve this? If you propose a fix, please make it concise.


for (const workspace of collections.v2WorkspaceLocalState.state.values()) {
if (
workspace.sidebarState.projectId === projectId &&
workspace.sidebarState.sectionId === null &&
workspace.sidebarState.tabOrder > anchorTabOrder
) {
const nextOrder = workspace.sidebarState.tabOrder + 1;
collections.v2WorkspaceLocalState.update(
workspace.workspaceId,
(draft) => {
draft.sidebarState.tabOrder = nextOrder;
},
);
}
}
for (const section of collections.v2SidebarSections.state.values()) {
if (
section.projectId === projectId &&
section.tabOrder > anchorTabOrder
) {
const nextOrder = section.tabOrder + 1;
collections.v2SidebarSections.update(section.sectionId, (draft) => {
draft.tabOrder = nextOrder;
});
}
}

tabOrder = anchorTabOrder + 1;
Comment on lines +199 to +236
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent fallback when insertAfterWorkspaceId is unknown.

If insertAfterWorkspaceId does not resolve to a workspace (e.g., stale id, race between menu open and state change), anchorWorkspace is undefined, anchorTabOrder falls back to 0, every top-level workspace/section in the project gets bumped +1, and the new section is inserted at tabOrder = 1 (top of the project). That's a surprising outcome for a "Create Section Below X" action — it would look like the section was created at the top and unrelated items shifted.

Consider short-circuiting (or falling through to the append-at-end branch) when the anchor workspace is missing or belongs to a different projectId:

🛡️ Proposed defensive check
-		if (insertAfterWorkspaceId) {
-			const anchorWorkspace = collections.v2WorkspaceLocalState.get(
-				insertAfterWorkspaceId,
-			);
-			const anchorTabOrder = anchorWorkspace?.sidebarState.sectionId
-				? (collections.v2SidebarSections.get(
-						anchorWorkspace.sidebarState.sectionId,
-					)?.tabOrder ?? 0)
-				: (anchorWorkspace?.sidebarState.tabOrder ?? 0);
+		const anchorWorkspace = insertAfterWorkspaceId
+			? collections.v2WorkspaceLocalState.get(insertAfterWorkspaceId)
+			: undefined;
+		if (anchorWorkspace && anchorWorkspace.sidebarState.projectId === projectId) {
+			const anchorTabOrder = anchorWorkspace.sidebarState.sectionId
+				? (collections.v2SidebarSections.get(
+						anchorWorkspace.sidebarState.sectionId,
+					)?.tabOrder ?? 0)
+				: anchorWorkspace.sidebarState.tabOrder;
📝 Committable suggestion

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

Suggested change
if (insertAfterWorkspaceId) {
const anchorWorkspace = collections.v2WorkspaceLocalState.get(
insertAfterWorkspaceId,
);
const anchorTabOrder = anchorWorkspace?.sidebarState.sectionId
? (collections.v2SidebarSections.get(
anchorWorkspace.sidebarState.sectionId,
)?.tabOrder ?? 0)
: (anchorWorkspace?.sidebarState.tabOrder ?? 0);
for (const workspace of collections.v2WorkspaceLocalState.state.values()) {
if (
workspace.sidebarState.projectId === projectId &&
workspace.sidebarState.sectionId === null &&
workspace.sidebarState.tabOrder > anchorTabOrder
) {
const nextOrder = workspace.sidebarState.tabOrder + 1;
collections.v2WorkspaceLocalState.update(
workspace.workspaceId,
(draft) => {
draft.sidebarState.tabOrder = nextOrder;
},
);
}
}
for (const section of collections.v2SidebarSections.state.values()) {
if (
section.projectId === projectId &&
section.tabOrder > anchorTabOrder
) {
const nextOrder = section.tabOrder + 1;
collections.v2SidebarSections.update(section.sectionId, (draft) => {
draft.tabOrder = nextOrder;
});
}
}
tabOrder = anchorTabOrder + 1;
const anchorWorkspace = insertAfterWorkspaceId
? collections.v2WorkspaceLocalState.get(insertAfterWorkspaceId)
: undefined;
if (anchorWorkspace && anchorWorkspace.sidebarState.projectId === projectId) {
const anchorTabOrder = anchorWorkspace.sidebarState.sectionId
? (collections.v2SidebarSections.get(
anchorWorkspace.sidebarState.sectionId,
)?.tabOrder ?? 0)
: anchorWorkspace.sidebarState.tabOrder;
for (const workspace of collections.v2WorkspaceLocalState.state.values()) {
if (
workspace.sidebarState.projectId === projectId &&
workspace.sidebarState.sectionId === null &&
workspace.sidebarState.tabOrder > anchorTabOrder
) {
const nextOrder = workspace.sidebarState.tabOrder + 1;
collections.v2WorkspaceLocalState.update(
workspace.workspaceId,
(draft) => {
draft.sidebarState.tabOrder = nextOrder;
},
);
}
}
for (const section of collections.v2SidebarSections.state.values()) {
if (
section.projectId === projectId &&
section.tabOrder > anchorTabOrder
) {
const nextOrder = section.tabOrder + 1;
collections.v2SidebarSections.update(section.sectionId, (draft) => {
draft.tabOrder = nextOrder;
});
}
}
tabOrder = anchorTabOrder + 1;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts`
around lines 199 - 236, The current branch assumes insertAfterWorkspaceId
resolves to a valid workspace and uses a default anchorTabOrder of 0, causing
mass reorders if the id is stale; update the logic in useDashboardSidebarState
so you first retrieve anchorWorkspace and guard that it exists and its
sidebarState.projectId === projectId (check insertAfterWorkspaceId,
anchorWorkspace, and anchorWorkspace.sidebarState.projectId), and if the guard
fails fall through to the append-at-end branch (i.e., treat as no
insertAfterWorkspaceId) instead of computing anchorTabOrder = 0 and bumping
every item in collections.v2WorkspaceLocalState / collections.v2SidebarSections;
keep the subsequent reordering code unchanged but only execute it when the
anchor is valid.

} else {
const sectionOrders = Array.from(
collections.v2SidebarSections.state.values(),
).filter((item) => item.projectId === projectId);
tabOrder = getNextTabOrder(sectionOrders);
}

collections.v2SidebarSections.insert({
sectionId,
projectId,
name,
createdAt: new Date(),
tabOrder: getNextTabOrder(sectionOrders),
tabOrder,
isCollapsed: false,
color: randomColor,
});
Expand Down
Loading