fix(app): tabs can be renamed again after a rename (#57322)

This commit is contained in:
Kit Langton
2026-10-20 00:04:23 -07:00
committed by GitHub
Unverified
parent 6f54deb9a5
commit b1bfe84d62
3 changed files with 41 additions and 30 deletions
+34 -23
View File
@@ -124,31 +124,42 @@ test("inactive session attention dots share the close control without marking th
await expectPath(page, sessionHref(a.id))
})
test("a tab does not reopen its title editor while a rename is saving", async ({ page }) => {
await mockWorkspace(page, { name: "Tabs", sessions: [a, b] })
await page.goto(sessionHref(a.id))
const title = page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(b.id)}"]) [data-slot="tab-title"]`)
const editor = page.locator('[data-slot="tab-title"][contenteditable="true"]')
await expect(title).toHaveText(b.title)
const save = await holdRoute(page, (url) => url.pathname === `/api/session/${b.id}`, { method: "PATCH" })
for (const tabLayout of ["horizontal", "vertical"] as const) {
test(`a ${tabLayout} tab can be renamed again while its previous rename is still saving`, async ({ page }) => {
await mockWorkspace(page, { name: "Tabs", sessions: [a, b], seed: { settings: { appearance: { tabLayout } } } })
await page.goto(sessionHref(a.id))
const title = page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(a.id)}"]) [data-slot="tab-title"]`)
const editor = page.locator('[data-slot="tab-title"][contenteditable="true"]')
await expect(title).toHaveText(a.title)
const save = await holdRoute(page, (url) => url.pathname === `/api/session/${a.id}`, { method: "PATCH" })
await title.dblclick()
await expect(editor).toBeFocused()
await editor.fill("Renamed tab")
await editor.press("Enter")
expect((await save.arrived).postDataJSON()).toEqual({ title: "Renamed tab" })
await expect(editor).toHaveCount(0)
await title.dblclick()
await expect(editor).toHaveCount(0)
await title.dblclick()
await expect(editor).toBeFocused()
await editor.fill("First name")
await editor.press("Enter")
expect((await save.arrived).postDataJSON()).toEqual({ title: "First name" })
await expect(editor).toHaveCount(0)
// The context menu's Rename item is enabled again once the save settles.
save.release()
await title.click({ button: "right" })
await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeEnabled()
await page.keyboard.press("Escape")
await title.dblclick()
await expect(editor).toBeFocused()
})
await title.dblclick()
await expect(editor).toBeFocused()
await editor.fill("Second name")
await editor.press("Enter")
await expect(title).toHaveText("Second name")
await title.click({ button: "right" })
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
await expect(editor).toBeFocused()
await editor.fill("Third name")
await editor.press("Enter")
await expect(title).toHaveText("Third name")
save.release()
await expect
.poll(() => save.requests.map((request) => request.postDataJSON()))
.toEqual([{ title: "First name" }, { title: "Second name" }, { title: "Third name" }])
await expect(title).toHaveText("Third name")
})
}
test("keyboard navigation follows the visible tab order and skips unresolved tabs", async ({ page }) => {
await mockWorkspace(page, { name: "Tabs", sessions: [a, c], seed: { tabs: [a.id, "ses_tab_unresolved", c.id] } })
+4 -6
View File
@@ -3,7 +3,6 @@ import { createStore } from "solid-js/store"
import { Predicate } from "effect"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { createMutation } from "@tanstack/solid-query"
import { IconButton } from "@opencode/ui/icon-button"
import { Icon } from "@opencode/ui/icon"
import { Menu } from "@opencode/ui/menu"
@@ -48,7 +47,6 @@ export function TabNavItem(props: {
let tabRoot!: HTMLDivElement
let titleEl!: HTMLSpanElement
let measureFrame: number | undefined
const rename = createMutation(() => ({ mutationFn: props.onRename }))
const closeTab = (event: MouseEvent) => {
event.preventDefault()
@@ -162,7 +160,7 @@ export function TabNavItem(props: {
}
const closeRename = async (save: boolean) => {
if (rename.isPending || !editing()) return
if (!editing()) return
const original = props.session?.title ?? ""
const next = (titleEl.textContent ?? "").trim()
@@ -174,7 +172,7 @@ export function TabNavItem(props: {
return
}
await rename.mutateAsync(next)
await props.onRename(next)
}
createEffect(() => {
@@ -191,7 +189,7 @@ export function TabNavItem(props: {
event?.preventDefault()
event?.stopPropagation()
if (props.dragging || editing() || rename.isPending) return
if (props.dragging || editing()) return
const session = props.session
if (!session) return
@@ -433,7 +431,7 @@ export function TabNavItem(props: {
openRename()
}}
>
<Menu.Item disabled={!props.session || rename.isPending} onSelect={() => setMenu("rename", true)}>
<Menu.Item disabled={!props.session} onSelect={() => setMenu("rename", true)}>
{language.t("common.rename")}
</Menu.Item>
<Menu.Item onSelect={props.onClose}>{language.t("common.closeTab")}</Menu.Item>
@@ -146,7 +146,9 @@ function SessionTabEntry(props: {
const current = session()
const currentCtx = props.serverCtx
if (current && currentCtx) currentCtx.data.session.remember({ ...current, title: value.title })
// A newer rename may have replaced this title while the save was in flight.
if (current && currentCtx && current.title === title)
currentCtx.data.session.remember({ ...current, title: value.title })
showToast({
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : undefined,