From d274dc54318b1767016f1c63249114af9810b938 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 10 Nov 2026 10:04:51 +0100 Subject: [PATCH] fix(app): hold a saved inline title against the title it replaced The field closed once the row's title moved off the one editing started from. When another client renamed the workspace while the field was open, that title had already moved, so a save closed the field on the daemon's answer and showed the other client's title until the saved one arrived. The session now records the title at submit and waits for it to change. --- .../browser/sidebar-workspace-rename.spec.ts | 55 +++++++++++++++++-- .../internal/use-sidebar-workspace-rename.tsx | 28 ++++++---- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts b/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts index e733f8004d..2a10e52c03 100644 --- a/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts +++ b/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts @@ -41,6 +41,22 @@ async function waitForTwoFrames(page: Page): Promise { }); } +type DaemonWebSocketGate = Awaited>; + +/** Sets the title from a second daemon client and waits until this page has rendered it. */ +async function renameFromAnotherClient( + page: Page, + gate: DaemonWebSocketGate, + workspace: Awaited>, + title: string, +): Promise { + gate.holdNextServerMessage("workspace_update"); + await workspace.client.setWorkspaceTitle(workspace.workspaceId, title); + await gate.waitForHeldServerMessage("workspace_update"); + gate.releaseHeldServerMessage("workspace_update"); + await waitForTwoFrames(page); +} + interface Box { x: number; y: number; @@ -190,12 +206,7 @@ test.describe("Sidebar workspace rename", () => { await expect(input).toBeFocused(); await expect(input).toHaveValue("main"); - // Another client renames the workspace while this field is open and untouched. - gate.holdNextServerMessage("workspace_update"); - await workspace.client.setWorkspaceTitle(workspace.workspaceId, "External title"); - await gate.waitForHeldServerMessage("workspace_update"); - gate.releaseHeldServerMessage("workspace_update"); - await waitForTwoFrames(page); + await renameFromAnotherClient(page, gate, workspace, "External title"); await page.mouse.click(900, 700); await expect(input).toHaveCount(0); @@ -206,6 +217,38 @@ test.describe("Sidebar workspace rename", () => { } }); + test("saving after a rename elsewhere holds the submitted title until it arrives", async ({ + page, + }) => { + const workspace = await seedWorkspace({ repoPrefix: "sidebar-rename-external-save-" }); + + try { + const gate = await installDaemonWebSocketGate(page); + await page.setViewportSize({ width: 1280, height: 800 }); + await gotoAppShell(page); + const row = page.getByTestId(workspaceRowTestId(workspace.workspaceId)); + const input = page.getByTestId(workspaceRenameInputTestId(workspace.workspaceId)); + + await chooseRenameFromKebab(page, workspace.workspaceId); + await expect(input).toBeFocused(); + await renameFromAnotherClient(page, gate, workspace, "External title"); + + await input.fill("Submitted title"); + gate.holdNextServerMessage("workspace_update"); + await page.keyboard.press("Enter"); + await gate.waitForHeldServerMessage("workspace_update"); + + await expect(input).toHaveValue("Submitted title"); + await expect(row).not.toContainText("External title"); + + gate.releaseHeldServerMessage("workspace_update"); + await expect(input).toHaveCount(0); + await expect(row).toContainText("Submitted title"); + } finally { + await workspace.cleanup(); + } + }); + test("below the lg breakpoint renaming opens the modal", async ({ page }) => { const workspace = await seedWorkspace({ repoPrefix: "sidebar-rename-modal-" }); diff --git a/packages/app/src/workspace-rename/internal/use-sidebar-workspace-rename.tsx b/packages/app/src/workspace-rename/internal/use-sidebar-workspace-rename.tsx index 496f79c4af..dd27e9a508 100644 --- a/packages/app/src/workspace-rename/internal/use-sidebar-workspace-rename.tsx +++ b/packages/app/src/workspace-rename/internal/use-sidebar-workspace-rename.tsx @@ -19,14 +19,13 @@ export interface SidebarRenamableWorkspace extends RenamableWorkspace { * * `saving` and `saved` keep the field on screen holding the submitted text. The daemon answers * the request before it pushes the updated workspace, so closing on the answer would show the - * old title for a beat. `saved` ends when the row's title moves off the one editing started from. + * old title for a beat. `saved` ends when the row's title moves off the one it had at submit, + * which is not the one editing started from when another client renamed it in between. */ -interface InlineRenameSession { - kind: "inline"; - phase: "editing" | "saving" | "saved"; - initialValue: string; - titleAtStart: string | null; -} +type InlineRenameSession = { kind: "inline"; initialValue: string } & ( + | { phase: "editing" } + | { phase: "saving" | "saved"; titleBeforeSave: string | null } +); type RenameSession = { kind: "closed" } | { kind: "modal" } | InlineRenameSession; @@ -52,7 +51,7 @@ export function useSidebarWorkspaceRename( if ( session.kind === "inline" && session.phase === "saved" && - currentTitle !== session.titleAtStart + currentTitle !== session.titleBeforeSave ) { setSession(CLOSED); } @@ -61,10 +60,10 @@ export function useSidebarWorkspaceRename( const begin = useCallback(() => { setSession( resolveWorkspaceRenameSurface() === "inline" - ? { kind: "inline", phase: "editing", initialValue, titleAtStart: currentTitle } + ? { kind: "inline", phase: "editing", initialValue } : { kind: "modal" }, ); - }, [currentTitle, initialValue]); + }, [initialValue]); const close = useCallback(() => setSession(CLOSED), []); @@ -76,7 +75,12 @@ export function useSidebarWorkspaceRename( setSession(CLOSED); return; } - setSession({ ...session, phase: "saving" }); + setSession({ + kind: "inline", + initialValue: session.initialValue, + phase: "saving", + titleBeforeSave: currentTitle, + }); setWorkspaceTitle(title).then( () => setSession((previous) => @@ -90,7 +94,7 @@ export function useSidebarWorkspaceRename( }, ); }, - [session, setWorkspaceTitle, toast], + [currentTitle, session, setWorkspaceTitle, toast], ); const titleEditor =