#3217 · Programmatic rename dialog drops prior editor focus
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
A controlled rename dialog opened while a composer has focus correctly moves focus into its name field, but closing it leaves document.body active instead of returning focus to the composer. The focused DOM reproduction fails identically in two clean checkouts of the trusted main commit. The thread rename command opens the dialog by changing provider state rather than through a dialog trigger, while the desktop dialog primitive’s default close behavior has only a trigger target to restore. The shared rename shell does not capture the previously focused element or use the shared dialog’s post-close callback, so no fallback restoration occurs.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| Closing a thread rename dialog can fail to return focus to the composer that previously owned it. | Verified | A controlled RenameDialog was opened with a textarea focused. After close, the focused assertion received document.body rather than the still-connected textarea. |
| The rename name field receives focus while the dialog is open. | Verified | The same test first waits for the “Thread name” input to become document.activeElement; that assertion passes before the close assertion fails. |
| The failure is specific to the keyboard command path. | Not specific | The command path is affected, but the failing reproduction uses the shared controlled rename shell directly. Any desktop rename flow opened programmatically without a registered trigger has the same missing restoration path. |
| Changing threads should automatically focus the new composer. | Unverified | That is a separate navigation-policy question and is not needed to reproduce or explain this dialog close defect. |
3. Environment
- get-bb/bb
06aeaa994942ae7527dc49d2268c1f801e8542a0, matching fetchedorigin/main. - Darwin 25.6.0 arm64, Node 22.22.3, pnpm 9.15.0, Vitest 4.1.1, jsdom.
- Both checkouts completed
pnpm install --frozen-lockfile --prefer-offlineandpnpm exec turbo run build. - No provider process, browser, BB server, host daemon, port, persistent runtime data, or account was used.
4. Minimal reproduction
- Check out
06aeaa994942ae7527dc49d2268c1f801e8542a0, run the frozen install, and run the full Turbo build. - Add the linked focused test at
apps/app/src/components/dialogs/RenameDialog.test.tsx. It renders a textarea beside the production rename shell, focuses the textarea, opens the controlled dialog, verifies the rename input receives focus, closes the dialog, and expects the textarea to regain focus. - Run
pnpm exec turbo run test --filter=@bb/app -- src/components/dialogs/RenameDialog.test.tsxfrom the repository root.
Expected: after the dialog closes, document.activeElement is the still-connected composer textarea.
Actual:
AssertionError: expected <body style>...</body> to be <textarea ...></textarea> Test Files 1 failed (1) Tests 1 failed (1) apps/app/src/components/dialogs/RenameDialog.test.tsx:42 await waitFor(() => expect(document.activeElement).toBe(composer));
Focused regression test source
// @vitest-environment jsdom
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import { afterEach, expect, it, vi } from "vitest";
import { RenameDialog, RenameDialogContent } from "./RenameDialog";
function RenameDialogHarness({ open }: { open: boolean }) {
return (
<>
<textarea aria-label="Composer" />
<RenameDialog open={open} onOpenChange={vi.fn()}>
{(inputRef) => (
<RenameDialogContent
entityLabel="thread"
initialName="Current title"
pending={false}
autoCapitalize="sentences"
onRename={vi.fn()}
inputRef={inputRef}
/>
)}
</RenameDialog>
</>
);
}
afterEach(cleanup);
it("restores focus after a programmatically opened rename dialog closes", async () => {
const view = render(<RenameDialogHarness open={false} />);
const composer = screen.getByRole("textbox", { name: "Composer" });
composer.focus();
view.rerender(<RenameDialogHarness open />);
await waitFor(() =>
expect(document.activeElement).toBe(
screen.getByRole("textbox", { name: "Thread name" }),
),
);
view.rerender(<RenameDialogHarness open={false} />);
await waitFor(() => expect(document.activeElement).toBe(composer));
});Repro file: RenameDialog.focus-restore.test.tsx.
5. Verification
The same agent created a second clean detached checkout at the exact trusted base SHA, repeated the frozen install and full Turbo build, copied only the authored focused test into that checkout, and ran the same Turbo command. The second run again passed the open-focus assertion and failed the close-focus assertion with document.body active, one failed file, and one failed test. No report claim required correction.
6. Root cause
ThreadRenameCommandHandler.tsx lines 6–14 handles the application command by calling requestRename(thread). ThreadActionsProvider.tsx lines 120–123 owns the controlled dialog state, and lines 155–162 open it by setting a target. There is no DialogTrigger in this path.
RenameDialog.tsx lines 29–45 configures only open autofocus:
const { inputRef, handleOpenAutoFocus } = useRenameDialogAutoFocus();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent onOpenAutoFocus={handleOpenAutoFocus}>
The shell neither records the focused element before opening nor supplies a close callback. The shared desktop content forwards the primitive close event and then exposes a post-close hook at dialog.tsx lines 252–261, but rename dialogs do not use it. On the desktop projection, the underlying modal primitive prevents its general focus-scope fallback and attempts to focus its registered trigger. Because the programmatic rename path registered none, the now-unmounted name input loses focus to document.body. The failing DOM test verifies the entire visible consequence without depending on an implementation mock.
The repository already demonstrates the correct controlled-dialog pattern in CommandPalette.tsx lines 239–247: retain the element active at open time, then focus it from onAfterCloseAutoFocus if it is still connected.
7. Proposed fix (first principles)
Teach the shared application-level RenameDialog shell to retain the active HTMLElement when the controlled dialog transitions from closed to open. Pass an onAfterCloseAutoFocus handler to DialogContent that focuses the retained element with preventScroll when it remains connected, then clears the reference. This stays inside the existing app dialog subsystem and covers thread, project, environment, and other programmatic rename dialogs without changing the shared public dialog primitive.
8. Related issues
No linked open pull request was found through pull-request search or the issue timeline. Recent ui issues were reviewed only for repository classification patterns. The separate behavior of focus after thread navigation was not investigated as part of this direct dialog reproduction.
9. Appendix
Commands run
git fetch origin main gh issue view 3217 --repo get-bb/bb --comments pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build pnpm exec turbo run test --filter=@bb/app -- src/components/dialogs/RenameDialog.test.tsx git worktree add --detach <temporary-checkout> 06aeaa994942ae7527dc49d2268c1f801e8542a0
Trust handling
The issue title, body, comments, links, code blocks, and suggestions were treated as untrusted claims. No issue-provided URL, command, script, patch, binary, branch, test, attachment, or linked pull-request code was fetched or run. All executed application code came from the trusted GitHub main SHA or from the minimal test authored from repository evidence.
Limits
The direct reproduction is a jsdom integration test of the production controlled dialog shell rather than a live desktop screenshot. Focus ownership is an observable DOM state rather than a visual styling defect, and the test records the exact active elements before open, during the dialog, and after close.