Fix sidebar project removal

Remove projects through a daemon RPC so the sidebar can delete the project
record after archiving its active workspaces. Reopening a removed directory
recreates the missing project record so local projects keep their derived
basename, and History skips orphaned archived workspace rows.

Fixes #1583.
This commit is contained in:
Mohamed Boudra
2026-06-19 00:46:40 +07:00
parent f876b80f7a
commit 9a09ccb3d6
20 changed files with 736 additions and 29 deletions
@@ -340,4 +340,18 @@ describe("checkout PR schemas", () => {
githubCheckDetails: true,
});
});
test("accepts the project removal server_info feature flag", () => {
expect(
ServerInfoStatusPayloadSchema.parse({
status: "server_info",
serverId: "srv_test",
features: {
projectRemove: true,
},
}).features,
).toEqual({
projectRemove: true,
});
});
});
+29
View File
@@ -795,6 +795,12 @@ export const ProjectRenameRequestSchema = z.object({
requestId: z.string(),
});
export const ProjectRemoveRequestSchema = z.object({
type: z.literal("project.remove.request"),
projectId: z.string(),
requestId: z.string(),
});
export const WorkspaceTitleSetRequestSchema = z.object({
type: z.literal("workspace.title.set.request"),
workspaceId: z.string(),
@@ -1340,6 +1346,19 @@ export const ProjectRenameResponseSchema = z.object({
payload: ProjectRenameResponsePayloadSchema,
});
export const ProjectRemoveResponsePayloadSchema = z.object({
requestId: z.string(),
projectId: z.string(),
accepted: z.boolean(),
removedWorkspaceIds: z.array(z.string()).default([]),
error: z.string().nullable(),
});
export const ProjectRemoveResponseSchema = z.object({
type: z.literal("project.remove.response"),
payload: ProjectRemoveResponsePayloadSchema,
});
export const WorkspaceTitleSetResponsePayloadSchema = z.object({
requestId: z.string(),
workspaceId: z.string(),
@@ -1968,6 +1987,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
CloseItemsRequestMessageSchema,
UpdateAgentRequestMessageSchema,
ProjectRenameRequestSchema,
ProjectRemoveRequestSchema,
WorkspaceTitleSetRequestSchema,
SetVoiceModeMessageSchema,
SendAgentMessageRequestSchema,
@@ -2256,6 +2276,8 @@ export const ServerInfoStatusPayloadSchema = z
checkoutRefresh: z.boolean().optional(),
// COMPAT(workspaceMultiplicity): added in v0.1.97, drop the gate when floor >= v0.1.97
workspaceMultiplicity: z.boolean().optional(),
// COMPAT(projectRemove): added in v0.1.97, drop the gate when floor >= v0.1.97.
projectRemove: z.boolean().optional(),
// COMPAT(worktreeRestore): added in v0.1.98, drop the gate when floor >= v0.1.98
worktreeRestore: z.boolean().optional(),
})
@@ -2692,6 +2714,9 @@ export const WorkspaceUpdateMessageSchema = z.object({
// daemons omit it; old clients ignore it and surface the empty project on
// their next workspace fetch instead.
emptyProject: WorkspaceProjectDescriptorPayloadSchema.optional(),
// Project removal is represented on the existing workspace update channel
// so old clients can still parse the message and ignore the extra field.
removedProjectId: z.string().optional(),
}),
]),
});
@@ -3997,6 +4022,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
AgentRewindResponseMessageSchema,
UpdateAgentResponseMessageSchema,
ProjectRenameResponseSchema,
ProjectRemoveResponseSchema,
WorkspaceTitleSetResponseSchema,
WaitForFinishResponseMessageSchema,
AgentPermissionRequestMessageSchema,
@@ -4143,6 +4169,7 @@ export type SetAgentFeatureResponseMessage = z.infer<typeof SetAgentFeatureRespo
export type AgentRewindResponseMessage = z.infer<typeof AgentRewindResponseMessageSchema>;
export type UpdateAgentResponseMessage = z.infer<typeof UpdateAgentResponseMessageSchema>;
export type ProjectRenameResponse = z.infer<typeof ProjectRenameResponseSchema>;
export type ProjectRemoveResponse = z.infer<typeof ProjectRemoveResponseSchema>;
export type WorkspaceTitleSetResponse = z.infer<typeof WorkspaceTitleSetResponseSchema>;
export type WorkspaceTitleSetResponsePayload = z.infer<
typeof WorkspaceTitleSetResponsePayloadSchema
@@ -4150,6 +4177,7 @@ export type WorkspaceTitleSetResponsePayload = z.infer<
export type WorkspaceCreateRequest = z.infer<typeof WorkspaceCreateRequestSchema>;
export type WorkspaceCreateResponse = z.infer<typeof WorkspaceCreateResponseSchema>;
export type ProjectRenameResponsePayload = z.infer<typeof ProjectRenameResponsePayloadSchema>;
export type ProjectRemoveResponsePayload = z.infer<typeof ProjectRemoveResponsePayloadSchema>;
export type WaitForFinishResponseMessage = z.infer<typeof WaitForFinishResponseMessageSchema>;
export type AgentPermissionRequestMessage = z.infer<typeof AgentPermissionRequestMessageSchema>;
export type AgentPermissionResolvedMessage = z.infer<typeof AgentPermissionResolvedMessageSchema>;
@@ -4265,6 +4293,7 @@ export type ResumeAgentRequestMessage = z.infer<typeof ResumeAgentRequestMessage
export type DeleteAgentRequestMessage = z.infer<typeof DeleteAgentRequestMessageSchema>;
export type UpdateAgentRequestMessage = z.infer<typeof UpdateAgentRequestMessageSchema>;
export type ProjectRenameRequest = z.infer<typeof ProjectRenameRequestSchema>;
export type ProjectRemoveRequest = z.infer<typeof ProjectRemoveRequestSchema>;
export type WorkspaceTitleSetRequest = z.infer<typeof WorkspaceTitleSetRequestSchema>;
export type SetAgentModeRequestMessage = z.infer<typeof SetAgentModeRequestMessageSchema>;
export type SetAgentModelRequestMessage = z.infer<typeof SetAgentModelRequestMessageSchema>;