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
156 changes: 156 additions & 0 deletions apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,162 @@ export const createWorkspacesRouter = () => {
githubStatus: worktree.githubStatus ?? null,
};
}),

getWorktreesByProject: publicProcedure
.input(z.object({ projectId: z.string() }))
.query(({ input }) => {
const worktrees = db.data.worktrees.filter(
(wt) => wt.projectId === input.projectId,
);

return worktrees.map((wt) => {
const workspace = db.data.workspaces.find(
(w) => w.worktreeId === wt.id,
);
return {
...wt,
hasActiveWorkspace: workspace !== undefined,
workspace: workspace ?? null,
};
});
}),

openWorktree: publicProcedure
.input(
z.object({
worktreeId: z.string(),
name: z.string().optional(),
}),
)
.mutation(async ({ input }) => {
const worktree = db.data.worktrees.find(
(wt) => wt.id === input.worktreeId,
);
if (!worktree) {
throw new Error(`Worktree ${input.worktreeId} not found`);
}

// Check if worktree already has an active workspace
const existingWorkspace = db.data.workspaces.find(
(w) => w.worktreeId === input.worktreeId,
);
if (existingWorkspace) {
throw new Error("Worktree already has an active workspace");
}

const project = db.data.projects.find(
(p) => p.id === worktree.projectId,
);
if (!project) {
throw new Error(`Project ${worktree.projectId} not found`);
}

// Verify worktree still exists on disk
const exists = await worktreeExists(
project.mainRepoPath,
worktree.path,
);
if (!exists) {
throw new Error("Worktree no longer exists on disk");
}

const projectWorkspaces = db.data.workspaces.filter(
(w) => w.projectId === worktree.projectId,
);
const maxTabOrder =
projectWorkspaces.length > 0
? Math.max(...projectWorkspaces.map((w) => w.tabOrder))
: -1;

const workspace = {
id: nanoid(),
projectId: worktree.projectId,
worktreeId: worktree.id,
name: input.name ?? worktree.branch,
tabOrder: maxTabOrder + 1,
createdAt: Date.now(),
updatedAt: Date.now(),
lastOpenedAt: Date.now(),
};

await db.update((data) => {
data.workspaces.push(workspace);
data.settings.lastActiveWorkspaceId = workspace.id;

const p = data.projects.find((p) => p.id === worktree.projectId);
if (p) {
p.lastOpenedAt = Date.now();

if (p.tabOrder === null) {
const activeProjects = data.projects.filter(
(proj) => proj.tabOrder !== null,
);
const maxProjectTabOrder =
activeProjects.length > 0
? // biome-ignore lint/style/noNonNullAssertion: filter guarantees tabOrder is not null
Math.max(...activeProjects.map((proj) => proj.tabOrder!))
: -1;
p.tabOrder = maxProjectTabOrder + 1;
}
}
});

// Load setup configuration from the main repo
const setupConfig = loadSetupConfig(project.mainRepoPath);

return {
workspace,
initialCommands: setupConfig?.setup || null,
worktreePath: worktree.path,
projectId: project.id,
};
}),

close: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input }) => {
const workspace = db.data.workspaces.find((w) => w.id === input.id);

if (!workspace) {
throw new Error("Workspace not found");
}
Comment on lines +785 to +792
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

Inconsistent error handling pattern with delete procedure.

The close procedure throws an Error when workspace is not found (line 791), while the delete procedure returns { success: false, error: "Workspace not found" } (line 403). This inconsistency may cause different error handling behavior on the frontend.

Consider aligning the approach—either both should throw (letting tRPC handle the error response) or both should return a result object with an error field.

🔎 Option A: Return error object (matches delete)
 		close: publicProcedure
 			.input(z.object({ id: z.string() }))
 			.mutation(async ({ input }) => {
 				const workspace = db.data.workspaces.find((w) => w.id === input.id);
 
 				if (!workspace) {
-					throw new Error("Workspace not found");
+					return { success: false, error: "Workspace not found" };
 				}
📝 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
close: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input }) => {
const workspace = db.data.workspaces.find((w) => w.id === input.id);
if (!workspace) {
throw new Error("Workspace not found");
}
close: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input }) => {
const workspace = db.data.workspaces.find((w) => w.id === input.id);
if (!workspace) {
return { success: false, error: "Workspace not found" };
}
🤖 Prompt for AI Agents
In apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts around lines 785
to 792, the close procedure currently throws an Error when the workspace is not
found which is inconsistent with the delete procedure that returns { success:
false, error: "Workspace not found" }; change close to return the same result
object shape as delete (e.g., return { success: false, error: "Workspace not
found" }) instead of throwing, and update the procedure's return type (and any
callers/types) accordingly so both procedures use the same error-handling
pattern.


// Kill all terminal processes in this workspace
const terminalResult = await terminalManager.killByWorkspaceId(
input.id,
);

// Delete workspace record ONLY, keep worktree
await db.update((data) => {
data.workspaces = data.workspaces.filter((w) => w.id !== input.id);

// Check if project should be hidden (no more open workspaces)
const remainingWorkspaces = data.workspaces.filter(
(w) => w.projectId === workspace.projectId,
);
if (remainingWorkspaces.length === 0) {
const p = data.projects.find((p) => p.id === workspace.projectId);
if (p) {
p.tabOrder = null;
}
}

// Update active workspace if this was the active one
if (data.settings.lastActiveWorkspaceId === input.id) {
const sorted = data.workspaces
.slice()
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
data.settings.lastActiveWorkspaceId = sorted[0]?.id || undefined;
}
});

const terminalWarning =
terminalResult.failed > 0
? `${terminalResult.failed} terminal process(es) may still be running`
: undefined;

return { success: true, terminalWarning };
}),
});
};

Expand Down
Loading