Fix worktree creation and archive flow

Server: drop the synchronous branch-name generator dep, use the
worktree slug as the initial branch, and move first-agent branch
auto-naming to an async post-creation step backed by a structured
LLM call. Guards against renaming when the placeholder branch has
already been changed.

App: make workspace and worktree archive optimistic, rolling back
the local hide if the daemon call fails, and suppress incoming
workspace upserts while an archive is pending.
This commit is contained in:
Mohamed Boudra
2026-05-03 23:22:54 +07:00
parent c534c857a4
commit 7c85341a42
29 changed files with 733 additions and 229 deletions
+12 -9
View File
@@ -42,7 +42,6 @@ import {
normalizeWorkspaceDescriptor,
} from "@/stores/session-store";
import { useDraftStore } from "@/stores/draft-store";
import { isLocalWorktreeArchivePending } from "@/stores/checkout-git-actions-store";
import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store";
import { sendOsNotification } from "@/utils/os-notifications";
import { getIsAppActivelyVisible } from "@/utils/app-visibility";
@@ -60,7 +59,10 @@ import type { AttachmentMetadata } from "@/attachments/types";
import { splitComposerAttachmentsForSubmit } from "@/components/composer-attachments";
import { reconcilePreviousAgentStatuses } from "@/contexts/session-status-tracking";
import { patchWorkspaceScripts } from "@/contexts/session-workspace-scripts";
import { shouldSuppressWorkspaceUpsertForLocalArchive } from "@/contexts/session-workspace-upserts";
import {
clearWorkspaceArchivePending,
shouldSuppressWorkspaceForLocalArchive,
} from "@/contexts/session-workspace-upserts";
import { isNative } from "@/constants/platform";
import { useToast } from "@/contexts/toast-context";
import { toErrorMessage } from "@/utils/error-messages";
@@ -543,6 +545,9 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
for (const entry of payload.entries) {
const workspace = normalizeWorkspaceDescriptor(entry);
if (shouldSuppressWorkspaceForLocalArchive({ serverId, workspace })) {
continue;
}
workspaces.set(workspace.id, workspace);
}
@@ -1239,18 +1244,16 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
const unsubWorkspaceUpdate = client.on("workspace_update", (message) => {
if (message.type !== "workspace_update") return;
if (message.payload.kind === "remove") {
clearWorkspaceArchivePending({
serverId,
workspaceId: String(message.payload.id),
});
removeWorkspaceSetup({ serverId, workspaceId: String(message.payload.id) });
removeWorkspace(serverId, String(message.payload.id));
return;
}
const workspace = normalizeWorkspaceDescriptor(message.payload.workspace);
if (
shouldSuppressWorkspaceUpsertForLocalArchive({
serverId,
workspace,
isArchivePending: isLocalWorktreeArchivePending,
})
) {
if (shouldSuppressWorkspaceForLocalArchive({ serverId, workspace })) {
return;
}
mergeWorkspaces(serverId, [workspace]);
@@ -1,6 +1,11 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import type { WorkspaceDescriptor } from "@/stores/session-store";
import { shouldSuppressWorkspaceUpsertForLocalArchive } from "@/contexts/session-workspace-upserts";
import {
clearWorkspaceArchivePending,
isWorkspaceArchivePending,
markWorkspaceArchivePending,
shouldSuppressWorkspaceForLocalArchive,
} from "@/contexts/session-workspace-upserts";
const baseWorkspace: WorkspaceDescriptor = {
id: "/repo/worktree",
@@ -21,40 +26,83 @@ function workspace(input?: Partial<WorkspaceDescriptor>): WorkspaceDescriptor {
return { ...baseWorkspace, ...input };
}
describe("shouldSuppressWorkspaceUpsertForLocalArchive", () => {
it("suppresses archiving upserts for a locally pending archive", () => {
const isArchivePending = vi.fn(() => true);
describe("workspace archive pending suppression", () => {
it("tracks a locally pending workspace archive by id and directory", () => {
markWorkspaceArchivePending({
serverId: "server-1",
workspaceId: "/repo/worktree",
workspaceDirectory: "/repo/worktree",
});
expect(
shouldSuppressWorkspaceUpsertForLocalArchive({
isWorkspaceArchivePending({
serverId: "server-1",
workspace: workspace({ workspaceDirectory: "/repo/worktree" }),
isArchivePending,
workspaceId: "/repo/worktree",
}),
).toBe(true);
expect(isArchivePending).toHaveBeenCalledWith({
serverId: "server-1",
cwd: "/repo/worktree",
});
});
it("allows archiving upserts when this client did not start the archive", () => {
expect(
shouldSuppressWorkspaceUpsertForLocalArchive({
isWorkspaceArchivePending({
serverId: "server-1",
workspace: workspace(),
isArchivePending: () => false,
workspaceDirectory: "/repo/worktree",
}),
).toBe(false);
});
it("allows normal upserts while a local archive is pending", () => {
).toBe(true);
expect(
shouldSuppressWorkspaceUpsertForLocalArchive({
shouldSuppressWorkspaceForLocalArchive({
serverId: "server-1",
workspace: workspace({ archivingAt: null }),
isArchivePending: () => true,
}),
).toBe(true);
clearWorkspaceArchivePending({ serverId: "server-1", workspaceId: "/repo/worktree" });
expect(
isWorkspaceArchivePending({
serverId: "server-1",
workspaceId: "/repo/worktree",
}),
).toBe(false);
});
it("suppresses upserts for a locally pending archive", () => {
markWorkspaceArchivePending({
serverId: "server-1",
workspaceId: "/repo/worktree",
workspaceDirectory: "/repo/worktree",
});
expect(
shouldSuppressWorkspaceForLocalArchive({
serverId: "server-1",
workspace: workspace({ workspaceDirectory: "/repo/worktree" }),
}),
).toBe(true);
clearWorkspaceArchivePending({ serverId: "server-1", workspaceId: "/repo/worktree" });
});
it("allows upserts when this client did not start the archive", () => {
expect(
shouldSuppressWorkspaceForLocalArchive({
serverId: "server-1",
workspace: workspace(),
}),
).toBe(false);
});
it("suppresses stale normal upserts while a local archive is pending", () => {
markWorkspaceArchivePending({
serverId: "server-1",
workspaceId: "/repo/worktree",
workspaceDirectory: "/repo/worktree",
});
expect(
shouldSuppressWorkspaceForLocalArchive({
serverId: "server-1",
workspace: workspace({ archivingAt: null }),
}),
).toBe(true);
clearWorkspaceArchivePending({ serverId: "server-1", workspaceId: "/repo/worktree" });
});
});
@@ -1,15 +1,96 @@
import type { WorkspaceDescriptor } from "@/stores/session-store";
import { normalizeWorkspaceOpaqueId, normalizeWorkspacePath } from "@/utils/workspace-identity";
export function shouldSuppressWorkspaceUpsertForLocalArchive(input: {
interface PendingWorkspaceArchive {
workspaceId: string;
workspaceDirectory: string | null;
}
const pendingWorkspaceArchivesByServer = new Map<string, Map<string, PendingWorkspaceArchive>>();
function pendingArchiveKey(input: { serverId: string; workspaceId: string }): string {
return `${input.serverId.trim()}::${input.workspaceId.trim()}`;
}
export function markWorkspaceArchivePending(input: {
serverId: string;
workspaceId: string;
workspaceDirectory?: string | null;
}): void {
const serverId = input.serverId.trim();
const workspaceId = normalizeWorkspaceOpaqueId(input.workspaceId);
if (!serverId || !workspaceId) {
return;
}
const archives = pendingWorkspaceArchivesByServer.get(serverId) ?? new Map();
archives.set(pendingArchiveKey({ serverId, workspaceId }), {
workspaceId,
workspaceDirectory: normalizeWorkspacePath(input.workspaceDirectory),
});
pendingWorkspaceArchivesByServer.set(serverId, archives);
}
export function clearWorkspaceArchivePending(input: {
serverId: string;
workspaceId: string;
}): void {
const serverId = input.serverId.trim();
const workspaceId = normalizeWorkspaceOpaqueId(input.workspaceId);
if (!serverId || !workspaceId) {
return;
}
const archives = pendingWorkspaceArchivesByServer.get(serverId);
if (!archives) {
return;
}
archives.delete(pendingArchiveKey({ serverId, workspaceId }));
if (archives.size === 0) {
pendingWorkspaceArchivesByServer.delete(serverId);
}
}
export function isWorkspaceArchivePending(input: {
serverId: string;
workspaceId?: string | null;
workspaceDirectory?: string | null;
}): boolean {
const serverId = input.serverId.trim();
if (!serverId) {
return false;
}
const archives = pendingWorkspaceArchivesByServer.get(serverId);
if (!archives) {
return false;
}
const workspaceId = normalizeWorkspaceOpaqueId(input.workspaceId);
if (workspaceId && archives.has(pendingArchiveKey({ serverId, workspaceId }))) {
return true;
}
const workspaceDirectory = normalizeWorkspacePath(input.workspaceDirectory);
if (!workspaceDirectory) {
return false;
}
for (const archive of archives.values()) {
if (archive.workspaceDirectory === workspaceDirectory) {
return true;
}
}
return false;
}
export function shouldSuppressWorkspaceForLocalArchive(input: {
serverId: string;
workspace: WorkspaceDescriptor;
isArchivePending: (params: { serverId: string; cwd: string }) => boolean;
}): boolean {
return (
input.workspace.archivingAt !== null &&
input.isArchivePending({
serverId: input.serverId,
cwd: input.workspace.workspaceDirectory,
})
);
return isWorkspaceArchivePending({
serverId: input.serverId,
workspaceId: input.workspace.id,
workspaceDirectory: input.workspace.workspaceDirectory,
});
}