Skip to content

Fix AppKit sidebar double-click inline rename committing instantly (#9495) - #9798

Merged
austinywang merged 3 commits into
mainfrom
issue-9495-dblclick-rename-commit
Aug 8, 2026
Merged

austinywang merged 3 commits into
mainfrom
issue-9495-dblclick-rename-commit

Conversation

@austinywang

@austinywang austinywang commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #9495.

What was broken

Double-clicking a workspace name on the AppKit sidebar list (sidebar-appkit-list-experiment, default-on) created the inline rename field, gave it first responder, and then lost the field editor ~1 ms later, committing the pre-filled unchanged title. The field flashed and the user could never type.

Root cause, confirmed in code: beginInlineRename() called window.makeFirstResponder(renameField) (which begins the editing session) and then renameField.selectText(nil). The extra selectText(nil) re-enters AppKit's field-editor machinery, synchronously tears down the just-begun session, and fires controlTextDidEndEditing, which the row's forked SidebarRowInlineRenameField honored by committing stringValue — the untouched title.

Why the fix is structural, not a one-line patch

The AppKit list had forked the inline-rename engine instead of reusing the SwiftUI sidebar's canonical one (SidebarInlineRenameTextField + SidebarInlineRenameCoordinator + SidebarInlineRenameCommit). The fork had no once-only commit/cancel guarantee, no two-stage Escape, no IME (marked text) pass-through, committed stale stringValue instead of the live field-editor text, and treated every end-editing as a commit with no policy — so a spurious commit also wrote customTitle, converting an automatic title into a user title and freezing auto-naming (the workspace.customTitle.write source=user line in the issue's log).

Per the shared-behavior policy (one action path per behavior across entrypoints), this PR deletes the fork and routes the AppKit list through the same engine as the SwiftUI sidebar:

  • SidebarRowInlineRenameSession (new focused file): one rename session per edit, owning the shared SidebarInlineRenameTextField (focus + select-all happens once, when the field enters the window — the selectText(nil) restart is gone by construction), the shared SidebarInlineRenameCoordinator (Enter/double-Escape/focus-loss resolve at most once; IME composition passes through; commits live editor text), and the shared SidebarInlineRenameCommit policy (empty drafts and unchanged auto-titles resolve to no write).
  • SidebarWorkspaceRowTableCellView: isEditing is now derived from the session (renameSession != nil) instead of a mutable flag; the persistent hidden renameField subview is gone; suspension resolves the session before teardown so end-editing during teardown can never re-enter commit, while the write itself stays deferred past the table mutation (unchanged staging contract).
  • SidebarWorkspaceRowModel: gains hasUserCustomTitle (plumbed from the existing SidebarWorkspaceRowInput field) so the commit policy has the same baseline input as the SwiftUI path.

Behavior parity gained with the SwiftUI sidebar: select-all on focus, Enter commits live text once, first Escape moves the caret to the start / second Escape cancels, focus loss commits, IME-safe, and unchanged-title commits no longer freeze auto-naming.

Two-commit regression proof

  • Commit 1 adds cmuxTests/SidebarWorkspaceRowInlineRenameTests only — five behavior tests driving the real AppKit editing path (cell hosted in a window, commands dispatched through the shared field editor). Dispatched test-e2e.yml at the commit-1 SHA: red (run link in PR comments).
  • Commit 2 is the fix. Same dispatch at HEAD: green.

Existing suspension tests that poked the old field's internals (cell.isEditing = true, renameField.stringValue, field.onCommit?(...)) were updated in commit 2 to drive the real session (begin → type through the field editor → resolve), keeping their behavioral assertions (deferred commits through atomic reloads/detach) intact.

Localization audit

No new user-facing strings. The AppKit rename field now carries the same localized placeholder (commandPalette.rename.workspacePlaceholder) and accessibility label (sidebar.workspace.rename.field.accessibilityLabel) as the SwiftUI field — both keys already exist in Resources/Localizable.xcstrings. Web catalogs unaffected.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Note

Low Risk
Localized sidebar UI rename behavior with shared commit policy and regression tests; no auth, data, or persistence schema changes beyond existing custom-title writes.

Overview
Fixes #9495: double-click rename on the AppKit sidebar list no longer flashes and immediately commits the untouched title.

The AppKit row drops its forked SidebarRowInlineRenameField and routes inline rename through SidebarRowInlineRenameSession, which wires the same SidebarInlineRenameTextField, SidebarInlineRenameCoordinator, and SidebarInlineRenameCommit path as the SwiftUI sidebar. Focus and select-all happen when the field enters the window hierarchy—no post-focus selectText(_:) that restarted the field editor and triggered spurious commits.

SidebarWorkspaceRowModel now carries hasUserCustomTitle so unchanged auto-titles resolve to no customTitle write (avoiding frozen auto-naming). Suspension/detach resolves the session before teardown while still deferring writes past table mutations.

Adds SidebarWorkspaceRowInlineRenameTests and updates suspension/table tests to drive the real field-editor path.

Reviewed by Cursor Bugbot for commit ffd5b85. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes the AppKit sidebar bug where double‑click inline rename instantly committed the unchanged title, and fixes the editor box sizing glitch on focus. The AppKit list now reuses the SwiftUI inline‑rename engine for stable, IME‑safe editing with consistent behavior.

  • Bug Fixes

    • Removed the re‑entrant selection path; the field focuses and selects once, preventing the instant commit.
    • Commits use live field‑editor text; unchanged auto‑titles are a no‑op, so auto‑naming isn’t frozen.
    • Enter commits once; double Escape cancels; focus loss commits; IME composition passes through.
    • Pre‑sizes the rename field before attach and clears the shared field‑editor background to avoid a mis‑sized dark box on focus.
    • Added regression tests that drive a real AppKit editing path.
  • Refactors

    • Replaced the forked field with SidebarRowInlineRenameSession using the shared SidebarInlineRenameTextField, SidebarInlineRenameCoordinator, and SidebarInlineRenameCommit.
    • isEditing now derives from the active session; suspension resolves before teardown and defers writes past table mutations.
    • SidebarWorkspaceRowModel adds hasUserCustomTitle and is plumbed from input for commit policy parity.

Written for commit c3472d6. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved inline workspace renaming in the sidebar.
    • Pressing Enter commits changes, while Escape cancels without saving.
    • Renames now handle unchanged titles, focus loss, workspace changes, and temporary suspension more reliably.
    • User-customized workspace titles are preserved correctly during rename operations.
  • Tests

    • Added coverage for committing, canceling, focus behavior, and suspension during inline renaming.

austinywang and others added 2 commits August 6, 2026 23:00
Double-clicking a workspace name on the AppKit sidebar list creates the
inline rename field but tears the editing session down ~1ms later,
committing the untouched title. These tests drive the real AppKit
editing path (cell in a window, shared field editor, commands dispatched
through the field editor) and assert the intended behavior: begin keeps
the session alive without a write, Enter commits the live editor text
once, an unchanged title is a no-op, Escape cancels, and focus loss
commits the typed draft.

Test-only commit: CI is expected to go red until the fix lands.

Issue: #9495

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Double-clicking a workspace name on the AppKit sidebar list committed
the untouched title ~1ms after the rename field appeared: after
makeFirstResponder began the editing session, the follow-up
selectText(nil) re-entered the field-editor machinery, synchronously
fired controlTextDidEndEditing, and the row's forked
SidebarRowInlineRenameField honored it by committing stringValue.

Delete the fork and route the AppKit list through the SwiftUI sidebar's
engine, one rename session per edit (SidebarRowInlineRenameSession):

- SidebarInlineRenameTextField focuses and selects once, when the field
  enters the window; the selectText restart is gone by construction.
- SidebarInlineRenameCoordinator resolves Enter, double-Escape, and
  focus loss at most once, passes IME composition through, and commits
  the live field-editor text instead of a stale stringValue.
- SidebarInlineRenameCommit gives the AppKit path the same commit
  policy as SwiftUI: empty drafts and unchanged auto-titles resolve to
  no write, so a stray commit can never freeze auto-naming.
  SidebarWorkspaceRowModel gains hasUserCustomTitle (plumbed from
  SidebarWorkspaceRowInput) as the policy baseline.

Cell suspension resolves the session before teardown, so end-editing
during teardown can no longer re-enter commit while the write itself
stays deferred past the table mutation. isEditing is now derived from
the session instead of a mutable flag. Existing suspension tests that
poked the old field's internals now drive the real session through the
field editor.

Fixes #9495

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@austinywang

Copy link
Copy Markdown
Contributor Author

Two-commit regression proof (dispatched test-e2e.yml runs pinned to each SHA, both on the reporter-matching macOS 15 runner):

@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The AppKit sidebar now uses SidebarRowInlineRenameSession for workspace renaming. The session manages focus, commit, cancel, suspension, and custom-title handling. New tests cover rename interactions and suspension behavior.

Changes

AppKit inline workspace renaming

Layer / File(s) Summary
Rename session and title-state contract
Sources/Sidebar/AppKitList/Cells/SidebarRowInlineRenameSession.swift, Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowModel.swift, Sources/ContentView.swift
Adds the rename session and forwards hasUserCustomTitle through the row configuration.
Workspace row rename lifecycle
Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift
Replaces cell-owned rename state with a session, updates field layout, and handles commit, cancel, suspension, and workspace changes.
Rename behavior and suspension coverage
cmuxTests/SidebarWorkspaceRowInlineRenameTests.swift, cmuxTests/SidebarWorkspaceRowSuspensionTests.swift, cmuxTests/SidebarWorkspaceTableSuspensionTests.swift, cmuxTests/SidebarAppKitRowCellTests.swift, cmux.xcodeproj/project.pbxproj
Adds AppKit rename regression tests, updates suspension tests to use the active field, initializes custom-title state, and registers new sources in the Xcode project.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SidebarWorkspaceRowCellView
  participant SidebarRowInlineRenameSession
  participant SidebarInlineRenameTextField
  SidebarWorkspaceRowCellView->>SidebarRowInlineRenameSession: begin inline rename
  SidebarRowInlineRenameSession->>SidebarInlineRenameTextField: configure and focus field
  SidebarInlineRenameTextField->>SidebarRowInlineRenameSession: send commit or cancel
  SidebarRowInlineRenameSession->>SidebarWorkspaceRowCellView: resolve title once
Loading

Possibly related PRs

Suggested reviewers: azooz2003-bit, lawrencecchen

🚥 Pre-merge checks | ✅ 25
✅ Passed checks (25 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes directly address issue #9495 by preventing premature commits and restoring functional inline renaming.
Out of Scope Changes check ✅ Passed The code, model, project, and test changes are all related to the inline-rename fix and its regression coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Cmux Swift Actor Isolation ✅ Passed New UI rename session and cell are explicitly @MainActor; the row model remains a nonisolated value struct with only a Bool added, and no Sendable or background UI access was introduced.
Cmux Swift Blocking Runtime ✅ Passed The complete two-commit diff adds no semaphore, wait, sleep, delayed dispatch, sync, lock, timer, or polling primitive; production coordination uses MainActor and callbacks.
Cmux Browser Automation Off-Main ✅ Passed The PR changes only sidebar inline-rename files; the parent-to-HEAD diff adds no browser socket commands, WebKit waits, worker routing, or policy changes.
Cmux Expensive Synchronous Load ✅ Passed The complete production diff adds only AppKit rename UI/session plumbing and a model field; no agent-history loader, large-file parse, directory scan, or synchronous I/O was added or moved.
Cmux Cache Substitution Correctness ✅ Passed The PR adds no cache substitution: hasUserCustomTitle comes from live tab state, and the existing snapshot cache keeps a fresh-read fallback plus event-driven refresh.
Cmux No Hacky Sleeps ✅ Passed The diff changes only Swift files and Xcode project metadata; it introduces no covered TypeScript, JavaScript, shell, or build/runtime-script sleeps or fixed delays.
Cmux Algorithmic Complexity ✅ Passed The production diff adds session/state handling and fixed-size UI operations only; it adds no scalable collection scan, sort, join, or per-target rescan.
Cmux Swift Concurrency ✅ Passed The diff adds no new DispatchQueue, Task, Combine, or completion-handler async work; the @MainActor onResolve closure is an AppKit event boundary and existing Combine code is unchanged.
Cmux Swift @Concurrent ✅ Passed The PR adds only synchronous @MainActor rename-session/UI code and data plumbing; no changed nonisolated async, @concurrent annotation, or heavy async call site requires review.
Cmux Swift Package Boundaries ✅ Passed The patch adds AppKit rename-session and row wiring only; it reuses the unchanged AppKit-dependent engine and adds no standalone domain logic that needs a SwiftPM target.
Cmux Swiftpm Lockfiles ✅ Passed The diff changes no Package.swift, Package.resolved, .gitignore, workflow, or dependency files; the Xcode project change only adds Swift source references, not package references.
Cmux Swift Logging ✅ Passed The complete PR diff adds no print, debugPrint, dump, NSLog, Logger, or ad hoc file/stdout logging; the only changed log is cmuxDebugLog under #if DEBUG with boolean state.
Cmux User-Facing Error Privacy ✅ Passed The diff adds only generic workspace rename labels and a DEBUG focus diagnostic; it adds no user-facing error, alert, output, or sensitive implementation detail.
Cmux Full Internationalization ✅ Passed New workspace rename text uses String(localized:defaultValue:); both referenced catalog keys already contain translated values for all 19 Localizable.xcstrings locales, with no catalog or web local...
Cmux Swiftui State Layout ✅ Passed The SwiftUI diff only forwards existing value-snapshot state; added rename code is an AppKit bridge with no new ObservableObject, wrappers, GeometryReader, lazy-row store reference, or render-time...
Cmux Architecture Rethink ✅ Passed The PR removes the forked rename path, reuses shared field/coordinator/commit logic, and adds a MainActor session with snapshot inputs and one-shot resolution; no timing, polling, locks, observers,...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR production changes add no standalone window; NSWindow appears only in test fixtures, and scripts/lint_auxiliary_window_close_shortcuts.py passes.
Cmux Source Artifacts ✅ Passed All 8 changed paths are hand-written Swift source/tests or Xcode configuration; the diff adds no binary files, logs, screenshots, caches, scratch directories, or build artifacts.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The aggregate PR diff adds no test-only seam in Sources; existing applyModelProbeForTesting is unchanged, and new field/resolve APIs have production callers.
Cmux No Ambient Global State ✅ Passed The production diff adds only the constructable, @MainActor SidebarRowInlineRenameSession and instance-owned state; no new free API, global mutable var, static-only namespace, or singleton appears.
Title check ✅ Passed The title clearly identifies the AppKit sidebar double-click rename bug and the primary fix.
Description check ✅ Passed The description thoroughly explains the bug, root cause, structural fix, testing, and verification, but omits the template checklist and demo video sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-9495-dblclick-rename-commit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift`:
- Around line 1018-1022: Replace the cmuxDebugLog call in the beginInlineRename
debug probe with dlog, preserving the existing `#if` DEBUG guard and message
content; alternatively remove the probe. Ensure this Sidebar code does not use
cmuxDebugLog.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f6735a1b-1b84-4ded-ba4c-d0a44f554368

📥 Commits

Reviewing files that changed from the base of the PR and between 7daa9c6 and ffd5b85.

📒 Files selected for processing (9)
  • Sources/ContentView.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarRowInlineRenameSession.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowModel.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SidebarAppKitRowCellTests.swift
  • cmuxTests/SidebarWorkspaceRowInlineRenameTests.swift
  • cmuxTests/SidebarWorkspaceRowSuspensionTests.swift
  • cmuxTests/SidebarWorkspaceTableSuspensionTests.swift

Comment thread Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift
The attach-time focus grab sizes the shared field editor from the
field's current frame, and a zero-frame grab mis-sizes the editor's
dark box over the row — the same lifecycle SidebarRowChecklistItemLine
already documents and handles. Run the row layout pass before adding
the field so it enters the window with its title-slot frame, and clear
the field-editor background after attach, reusing the checklist's
helper.

Codex review finding on #9798.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@austinywang
austinywang merged commit f9a29b4 into main Aug 8, 2026
13 of 14 checks passed
@austinywang
austinywang deleted the issue-9495-dblclick-rename-commit branch August 8, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workspace double-click inline rename flashes and commits instantly on the AppKit sidebar

1 participant