Skip to content
Closed
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
32 changes: 21 additions & 11 deletions apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { workspaces, worktrees } from "@superset/local-db";
import { workspaces } from "@superset/local-db";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
Expand All @@ -13,6 +13,10 @@ import { getTerminalHostClient } from "main/lib/terminal-host/client";
import { getWorkspaceRuntimeRegistry } from "main/lib/workspace-runtime";
import { z } from "zod";
import { publicProcedure, router } from "../..";
import {
resolveWorktreePathOrThrowWithMetadata,
resolveWorktreePathWithRepair,
} from "../workspaces/utils/repair-worktree-path";
import { assertWorkspaceUsable } from "../workspaces/utils/usability";
import { resolveTerminalThemeType } from "./theme-type";
import { getWorkspaceTerminalContext, resolveCwd } from "./utils";
Expand Down Expand Up @@ -86,9 +90,19 @@ export const createTerminalRouter = () => {
themeType,
} = input;

const { workspace, workspacePath, rootPath } =
getWorkspaceTerminalContext(workspaceId);
if (workspace?.type === "worktree") {
const {
workspace,
workspacePath: baseWorkspacePath,
rootPath,
} = getWorkspaceTerminalContext(workspaceId);
let workspacePath = baseWorkspacePath;
let pathChanged = false;
if (workspace?.type === "worktree" && workspace.worktreeId) {
const resolved = await resolveWorktreePathOrThrowWithMetadata(
workspace.worktreeId,
);
workspacePath = resolved.path ?? undefined;
pathChanged = resolved.pathChanged;
assertWorkspaceUsable(workspaceId, workspacePath);
}
Comment on lines +100 to 107
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P2: Worktree usability checks are skipped when type === "worktree" but worktreeId is missing, allowing terminal creation with an invalid/missing workspace path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/terminal/terminal.ts, line 100:

<comment>Worktree usability checks are skipped when `type === "worktree"` but `worktreeId` is missing, allowing terminal creation with an invalid/missing workspace path.</comment>

<file context>
@@ -86,9 +90,19 @@ export const createTerminalRouter = () => {
+				} = getWorkspaceTerminalContext(workspaceId);
+				let workspacePath = baseWorkspacePath;
+				let pathChanged = false;
+				if (workspace?.type === "worktree" && workspace.worktreeId) {
+					const resolved = await resolveWorktreePathOrThrowWithMetadata(
+						workspace.worktreeId,
</file context>
Suggested change
if (workspace?.type === "worktree" && workspace.worktreeId) {
const resolved = await resolveWorktreePathOrThrowWithMetadata(
workspace.worktreeId,
);
workspacePath = resolved.path ?? undefined;
pathChanged = resolved.pathChanged;
assertWorkspaceUsable(workspaceId, workspacePath);
}
if (workspace?.type === "worktree") {
if (workspace.worktreeId) {
const resolved = await resolveWorktreePathOrThrowWithMetadata(
workspace.worktreeId,
);
workspacePath = resolved.path ?? undefined;
pathChanged = resolved.pathChanged;
}
assertWorkspaceUsable(workspaceId, workspacePath);
}
Fix with Cubic

const cwd = resolveCwd(cwdOverride, workspacePath);
Expand Down Expand Up @@ -142,6 +156,7 @@ export const createTerminalRouter = () => {
isNew: result.isNew,
scrollback: result.scrollback,
wasRecovered: result.wasRecovered,
pathChanged,
// Cold restore fields (for reboot recovery)
isColdRestore: result.isColdRestore,
previousCwd: result.previousCwd,
Expand Down Expand Up @@ -400,7 +415,7 @@ export const createTerminalRouter = () => {

getWorkspaceCwd: publicProcedure
.input(z.string())
.query(({ input: workspaceId }) => {
.query(async ({ input: workspaceId }) => {
const workspace = localDb
.select()
.from(workspaces)
Expand All @@ -414,12 +429,7 @@ export const createTerminalRouter = () => {
return null;
}

const worktree = localDb
.select()
.from(worktrees)
.where(eq(worktrees.id, workspace.worktreeId))
.get();
return worktree?.path ?? null;
return resolveWorktreePathWithRepair(workspace.worktreeId);
}),

stream: publicProcedure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
fetchGitHubPRStatus,
type PullRequestCommentsTarget,
} from "../utils/github";
import {
resolveWorktreePathOrThrow,
resolveWorktreePathWithRepair,
} from "../utils/repair-worktree-path";

const gitHubPRCommentsInputSchema = z.object({
workspaceId: z.string(),
Expand Down Expand Up @@ -107,8 +111,13 @@ export const createGitStatusProcedures = () => {

await fetchDefaultBranch(project.mainRepoPath, defaultBranch);

const worktreePath = await resolveWorktreePathOrThrow(worktree.id);
if (!worktreePath) {
throw new Error(`Worktree ${worktree.id} path could not be resolved`);
}

const { ahead, behind } = await getAheadBehindCount({
repoPath: worktree.path,
repoPath: worktreePath,
defaultBranch,
});

Expand Down Expand Up @@ -163,7 +172,12 @@ export const createGitStatusProcedures = () => {
return null;
}

const freshStatus = await fetchGitHubPRStatus(worktree.path);
const worktreePath = await resolveWorktreePathWithRepair(worktree.id);
if (!worktreePath) {
return null;
}

const freshStatus = await fetchGitHubPRStatus(worktreePath);

if (freshStatus) {
localDb
Expand Down Expand Up @@ -192,9 +206,13 @@ export const createGitStatusProcedures = () => {
}

const cachedGitHubStatus = worktree.githubStatus ?? null;
const worktreePath = await resolveWorktreePathWithRepair(worktree.id);
if (!worktreePath) {
return [];
}

return fetchGitHubPRComments({
worktreePath: worktree.path,
worktreePath,
pullRequest: resolveCommentsPullRequestTarget({
input,
githubStatus: cachedGitHubStatus,
Expand All @@ -204,7 +222,7 @@ export const createGitStatusProcedures = () => {

getWorktreeInfo: publicProcedure
.input(z.object({ workspaceId: z.string() }))
.query(({ input }) => {
.query(async ({ input }) => {
const workspace = getWorkspace(input.workspaceId);
if (!workspace) {
return null;
Expand All @@ -217,7 +235,8 @@ export const createGitStatusProcedures = () => {
return null;
}

const worktreeName = worktree.path.split("/").pop() ?? worktree.branch;
const resolvedPath = await resolveWorktreePathWithRepair(worktree.id);
const worktreeName = resolvedPath?.split("/").pop() ?? worktree.branch;
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P2: Use a cross-platform path utility (for example path.basename) instead of split("/"); this parsing fails for Windows worktree paths.

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

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts, line 239:

<comment>Use a cross-platform path utility (for example `path.basename`) instead of `split("/")`; this parsing fails for Windows worktree paths.

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

<file context>
@@ -217,7 +235,8 @@ export const createGitStatusProcedures = () => {
 
-				const worktreeName = worktree.path.split("/").pop() ?? worktree.branch;
+				const resolvedPath = await resolveWorktreePathWithRepair(worktree.id);
+				const worktreeName = resolvedPath?.split("/").pop() ?? worktree.branch;
 				const branchName = worktree.branch;
 
</file context>
Fix with Cubic

const branchName = worktree.branch;

return {
Expand Down
145 changes: 106 additions & 39 deletions apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { existsSync } from "node:fs";
import {
projects,
type SelectWorkspace,
workspaceSections,
workspaces,
worktrees,
Expand All @@ -11,11 +13,58 @@ import { z } from "zod";
import { publicProcedure, router } from "../../..";
import { getWorkspace } from "../utils/db-helpers";
import { getProjectChildItems } from "../utils/project-children-order";
import {
resolveWorktreePathWithRepairMetadata,
type TrackedWorktreeRepairState,
} from "../utils/repair-worktree-path";
import { loadSetupConfig } from "../utils/setup";
import { computeVisualOrder } from "../utils/visual-order";
import { getWorkspacePath } from "../utils/worktree";

type WorktreePathMap = Map<string, string>;
interface WorkspaceQueryPathState {
worktreePath: string;
existsOnDisk: boolean;
repairState: TrackedWorktreeRepairState;
repairMessage: string | null;
repairCommand: string | null;
}

async function resolveWorkspacePathState(input: {
workspace: SelectWorkspace;
mainRepoPath: string | null | undefined;
}): Promise<WorkspaceQueryPathState> {
if (input.workspace.type === "branch") {
const worktreePath = input.mainRepoPath ?? "";
return {
worktreePath,
existsOnDisk: !!worktreePath && existsSync(worktreePath),
repairState: "ok",
repairMessage: null,
repairCommand: null,
};
}

if (!input.workspace.worktreeId) {
return {
worktreePath: "",
existsOnDisk: false,
repairState: "missing",
repairMessage: "Tracked worktree could not be found.",
repairCommand: null,
};
}

const resolution = await resolveWorktreePathWithRepairMetadata(
input.workspace.worktreeId,
);

return {
worktreePath: resolution.path ?? "",
existsOnDisk: !!resolution.path && existsSync(resolution.path),
repairState: resolution.repairState,
repairMessage: resolution.repairMessage,
repairCommand: resolution.repairCommand,
};
}

/** Returns workspace IDs in sidebar visual order (by project.tabOrder, then ungrouped workspaces, then sections by tabOrder). */
function getWorkspacesInVisualOrder(): string[] {
Expand Down Expand Up @@ -61,11 +110,19 @@ export const createQueryProcedures = () => {
.where(eq(worktrees.id, workspace.worktreeId))
.get()
: null;
const pathState = await resolveWorkspacePathState({
workspace,
mainRepoPath: project?.mainRepoPath,
});

return {
...workspace,
type: workspace.type as "worktree" | "branch",
worktreePath: getWorkspacePath(workspace) ?? "",
worktreePath: pathState.worktreePath,
existsOnDisk: pathState.existsOnDisk,
repairState: pathState.repairState,
repairMessage: pathState.repairMessage,
repairCommand: pathState.repairCommand,
project: project
? {
id: project.id,
Expand Down Expand Up @@ -94,13 +151,17 @@ export const createQueryProcedures = () => {
.sort((a, b) => a.tabOrder - b.tabOrder);
}),

getAllGrouped: publicProcedure.query(() => {
getAllGrouped: publicProcedure.query(async () => {
type WorkspaceItem = {
id: string;
projectId: string;
sectionId: string | null;
worktreeId: string | null;
worktreePath: string;
existsOnDisk: boolean;
repairState: TrackedWorktreeRepairState;
repairMessage: string | null;
repairCommand: string | null;
type: "worktree" | "branch";
branch: string;
name: string;
Expand Down Expand Up @@ -134,11 +195,6 @@ export const createQueryProcedures = () => {
.where(isNotNull(projects.tabOrder))
.all();

const allWorktrees = localDb.select().from(worktrees).all();
const worktreePathMap: WorktreePathMap = new Map(
allWorktrees.map((wt) => [wt.id, wt.path]),
);

const allSections = localDb.select().from(workspaceSections).all();

const groupsMap = new Map<
Expand Down Expand Up @@ -199,38 +255,53 @@ export const createQueryProcedures = () => {
.all()
.sort((a, b) => a.tabOrder - b.tabOrder);

for (const workspace of allWorkspaces) {
const group = groupsMap.get(workspace.projectId);
if (group) {
let worktreePath = "";
if (workspace.type === "worktree" && workspace.worktreeId) {
worktreePath = worktreePathMap.get(workspace.worktreeId) ?? "";
} else if (workspace.type === "branch") {
worktreePath = group.project.mainRepoPath;
const resolvedWorkspaces = await Promise.all(
allWorkspaces.map(async (workspace) => {
const group = groupsMap.get(workspace.projectId);
if (!group) {
return null;
}

const pathState = await resolveWorkspacePathState({
workspace,
mainRepoPath: group.project.mainRepoPath,
});

const item: WorkspaceItem = {
...workspace,
sectionId: workspace.sectionId ?? null,
type: workspace.type as "worktree" | "branch",
worktreePath,
worktreePath: pathState.worktreePath,
existsOnDisk: pathState.existsOnDisk,
repairState: pathState.repairState,
repairMessage: pathState.repairMessage,
repairCommand: pathState.repairCommand,
isUnread: workspace.isUnread ?? false,
isUnnamed: workspace.isUnnamed ?? false,
};

if (workspace.sectionId) {
const section = group.sections.find(
(s) => s.id === workspace.sectionId,
);
if (section) {
section.workspaces.push(item);
} else {
// Orphan: section not found, fall back to ungrouped
group.workspaces.push(item);
}
return { workspace, group, item };
}),
);

for (const resolvedWorkspace of resolvedWorkspaces) {
if (!resolvedWorkspace) {
continue;
}

const { workspace, group, item } = resolvedWorkspace;
if (workspace.sectionId) {
const section = group.sections.find(
(s) => s.id === workspace.sectionId,
);
if (section) {
section.workspaces.push(item);
} else {
// Orphan: section not found, fall back to ungrouped
group.workspaces.push(item);
}
} else {
group.workspaces.push(item);
}
}

Expand Down Expand Up @@ -291,7 +362,7 @@ export const createQueryProcedures = () => {

getResolvedRunCommands: publicProcedure
.input(z.object({ workspaceId: z.string() }))
.query(({ input }) => {
.query(async ({ input }) => {
const workspace = localDb
.select()
.from(workspaces)
Expand All @@ -313,17 +384,13 @@ export const createQueryProcedures = () => {
return { commands: [] };
}

const worktree = workspace.worktreeId
? localDb
.select()
.from(worktrees)
.where(eq(worktrees.id, workspace.worktreeId))
.get()
: null;

const worktreePath =
workspace.type === "worktree" && worktree?.path
? worktree.path
workspace.type === "worktree" && workspace.worktreeId
? ((
await resolveWorktreePathWithRepairMetadata(
workspace.worktreeId,
)
).path ?? undefined)
: workspace.type === "branch"
? project.mainRepoPath
: undefined;
Expand Down
Loading