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
14 changes: 11 additions & 3 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/db-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,21 @@ export function hideProjectIfNoWorkspaces(projectId: string): void {
/**
* Select the next active workspace after the current one is removed.
* Returns the ID of the next workspace to activate, or null if none.
* Selects the most recently opened workspace (excluding those being deleted).
* Selects the most recently opened workspace from VISIBLE projects only
* (projects with tabOrder != null). This ensures the selected workspace
* will appear in the sidebar and can be properly displayed by the frontend.
*/
export function selectNextActiveWorkspace(): string | null {
const sorted = localDb
.select()
.select({ id: workspaces.id, lastOpenedAt: workspaces.lastOpenedAt })
.from(workspaces)
.where(isNull(workspaces.deletingAt))
.innerJoin(projects, eq(workspaces.projectId, projects.id))
.where(
and(
isNull(workspaces.deletingAt),
isNotNull(projects.tabOrder), // Only visible projects
),
)
.orderBy(desc(workspaces.lastOpenedAt))
.all();
return sorted[0]?.id ?? null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,42 +64,48 @@ export function useCloseWorkspace(
);
}

// If closing the active workspace, switch to another workspace optimistically
// This prevents a flash of "no workspace" state while the backend processes
// Switch to next workspace to prevent "no workspace" flash
if (previousActive?.id === id) {
// Find the next workspace to switch to (matches backend logic: most recently opened)
const remainingWorkspaces = previousAll
?.filter((w) => w.id !== id)
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);

if (remainingWorkspaces && remainingWorkspaces.length > 0) {
const nextWorkspace = remainingWorkspaces[0];
// Find the project info for the next workspace from grouped data
const projectGroup = previousGrouped?.find((g) =>
g.workspaces.some((w) => w.id === nextWorkspace.id),
);
const workspaceFromGrouped = projectGroup?.workspaces.find(
(w) => w.id === nextWorkspace.id,
);
// Find a workspace with full data available in previousGrouped
let selectedWorkspace = null;
let projectGroup = null;
let workspaceFromGrouped = null;

if (projectGroup && workspaceFromGrouped) {
// For worktree-type workspaces, provide minimal worktree data to prevent
// hasIncompleteInit from triggering the initialization view
for (const candidate of remainingWorkspaces) {
const group = previousGrouped?.find((g) =>
g.workspaces.some((w) => w.id === candidate.id),
);
if (group) {
selectedWorkspace = candidate;
projectGroup = group;
workspaceFromGrouped = group.workspaces.find(
(w) => w.id === candidate.id,
);
break;
}
}

if (selectedWorkspace && projectGroup && workspaceFromGrouped) {
const worktreeData =
workspaceFromGrouped.type === "worktree"
? {
branch: nextWorkspace.branch,
branch: selectedWorkspace.branch,
baseBranch: null,
gitStatus: {
branch: nextWorkspace.branch,
branch: selectedWorkspace.branch,
needsRebase: false,
lastRefreshed: Date.now(),
},
}
: null;

utils.workspaces.getActive.setData(undefined, {
...nextWorkspace,
...selectedWorkspace,
type: workspaceFromGrouped.type,
worktreePath: workspaceFromGrouped.worktreePath,
project: {
Expand All @@ -110,11 +116,17 @@ export function useCloseWorkspace(
worktree: worktreeData,
});
} else {
// Fallback: just clear it and let invalidate handle it
utils.workspaces.getActive.setData(undefined, null);
// Fallback: set minimal data to prevent StartView flash (refetch will populate full data)
const fallback = remainingWorkspaces[0];
utils.workspaces.getActive.setData(undefined, {
...fallback,
type: fallback.type === "branch" ? "branch" : "worktree",
worktreePath: "",
project: null,
worktree: null,
});
}
} else {
// No remaining workspaces
utils.workspaces.getActive.setData(undefined, null);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,30 +66,41 @@ export function useDeleteWorkspace(
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);

if (remainingWorkspaces && remainingWorkspaces.length > 0) {
const nextWorkspace = remainingWorkspaces[0];
const projectGroup = previousGrouped?.find((g) =>
g.workspaces.some((w) => w.id === nextWorkspace.id),
);
const workspaceFromGrouped = projectGroup?.workspaces.find(
(w) => w.id === nextWorkspace.id,
);
// Find a workspace with full data available in previousGrouped
let selectedWorkspace = null;
let projectGroup = null;
let workspaceFromGrouped = null;

if (projectGroup && workspaceFromGrouped) {
for (const candidate of remainingWorkspaces) {
const group = previousGrouped?.find((g) =>
g.workspaces.some((w) => w.id === candidate.id),
);
if (group) {
selectedWorkspace = candidate;
projectGroup = group;
workspaceFromGrouped = group.workspaces.find(
(w) => w.id === candidate.id,
);
break;
}
}

if (selectedWorkspace && projectGroup && workspaceFromGrouped) {
const worktreeData =
workspaceFromGrouped.type === "worktree"
? {
branch: nextWorkspace.branch,
branch: selectedWorkspace.branch,
baseBranch: null,
gitStatus: {
branch: nextWorkspace.branch,
branch: selectedWorkspace.branch,
needsRebase: false,
lastRefreshed: Date.now(),
},
}
: null;

utils.workspaces.getActive.setData(undefined, {
...nextWorkspace,
...selectedWorkspace,
type: workspaceFromGrouped.type,
worktreePath: workspaceFromGrouped.worktreePath,
project: {
Expand All @@ -100,7 +111,15 @@ export function useDeleteWorkspace(
worktree: worktreeData,
});
} else {
utils.workspaces.getActive.setData(undefined, null);
// Fallback: set minimal data to prevent StartView flash (refetch will populate full data)
const fallback = remainingWorkspaces[0];
utils.workspaces.getActive.setData(undefined, {
...fallback,
type: fallback.type === "branch" ? "branch" : "worktree",
worktreePath: "",
project: null,
worktree: null,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fallback triggers initialization view for worktree workspaces

Medium Severity

The fallback sets worktree: null while also setting type to "worktree" for non-branch workspaces. This combination causes hasIncompleteInit to evaluate to true in WorkspaceView (since worktree?.gitStatus becomes undefined), which displays WorkspaceInitializingView instead of the normal content. The main path provides minimal worktreeData with a gitStatus object for worktree types, but this fallback doesn't, defeating the goal of preventing incorrect views during workspace transitions.

Additional Locations (1)

Fix in Cursor Fix in Web

}
} else {
utils.workspaces.getActive.setData(undefined, null);
Expand Down
Loading