Repository navigation
Route the checklist's first item through the popover in popover style - #7797
azooz2003-bit merged 17 commits into
Conversation
usesPopoverPresentation && totalCount > 0 forced inline entry for a workspace's very first checklist item regardless of the popover setting, since an empty checklist has no summary line to anchor a popover to. Move the popover anchor from the summary line to the section container (stable across the 0->1 item transition), route the ghost "Add item" button through the same popover-toggle path as the summary line, and fix the "Add Checklist Item..." notification handler in ContentView.swift (it special-cased non-empty checklists, so the context-menu/palette path still fell back to inline for a fresh workspace even after the view-level fix). Inline style is unaffected.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR makes the checklist popover handle empty checklists and first-item entry. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (14): Last reviewed commit: "Address review: keep popover host mounte..." | Re-trigger Greptile |
| /// below, which is present whether or not a summary line exists yet. | ||
| private var presentsPopover: Bool { | ||
| usesPopoverPresentation && totalCount > 0 | ||
| usesPopoverPresentation |
There was a problem hiding this comment.
Activation Token Stays Pending
When an empty checklist is opened through the new popover path and then dismissed without adding an item, the inline cancel path no longer runs and the popover close path does not consume addFieldActivationToken. That leaves checklistAddFieldActivationTokens[workspaceId] > 0, so an otherwise-empty checklist section can stay rendered indefinitely just because of stale add state.
Rule Used: Flag SwiftUI changes that can cause stale state, b... (source)
| if presentsPopover { | ||
| onPopoverPresentedChange(!isPopoverPresented) | ||
| } else { |
There was a problem hiding this comment.
For an empty checklist opened by the add-item notification, the ghost “Add item” row remains visible while the popover is already open. Clicking it sends false here and closes the popover instead of keeping the add field available, so the add affordance behaves like a dismiss action.
| if presentsPopover { | |
| onPopoverPresentedChange(!isPopoverPresented) | |
| } else { | |
| if presentsPopover { | |
| onPopoverPresentedChange(true) | |
| } else { |
Rule Used: Flag SwiftUI changes that can cause stale state, b... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
taskStatus is nil whenever a workspace's status is hidden (statusHidden), which restore-workspace-todos-default-none made the default for all new workspaces. The checklist section's render gate required taskStatus != nil, so "Add Checklist Item..." silently did nothing for any new workspace. Drop that condition; the checklist section only needs its own content/token gate.
| Button { | ||
| isAddingItem = true | ||
| if presentsPopover { | ||
| onPopoverPresentedChange(!isPopoverPresented) | ||
| } else { | ||
| isAddingItem = true | ||
| } |
There was a problem hiding this comment.
Add Row Dismisses In an empty checklist,
expandedList keeps this ghost add row visible while the popover is already open. Clicking “Add item” then sends false through this toggle and closes the popover instead of keeping the add field available. The row should open the popover when closed and avoid dismissing it when it is already presented.
The only way to delete a checklist item was a hidden right-click
"Remove" context menu entry. Add a trailing xmark.circle.fill button
that appears on row hover (reserved layout space, opacity/hit-test
toggle only, so row height never jumps) in both row implementations:
the inline sidebar row and the NSPopover-hosted row. Both call the
existing actions.removeItem(_:) closure, the same one the context
menu already used; the context menu entry is unchanged.
New localized tooltip key sidebar.checklist.removeItemTooltip
("Remove item" / "項目を削除").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… add keyboard delete - Highlighting a checklist item (click, Up/Down) and typing a new item in the add field are now mutually exclusive: a non-empty draft clears any highlight, and highlight-setting paths no-op while a draft exists. Makes Return's outcome (toggle item vs. commit new item) unambiguous. - Popover-style checklists no longer show a persistent ghost "Add item" row inline when a workspace has zero items. "Add Checklist Item…" now opens the popover directly with nothing shown inline until an item is actually added, at which point the existing summary line is the small status preview. - Item rows now center-align the checkbox and text (was firstTextBaseline, which misaligned the checkbox glyph against the text's baseline). - Backspace with an empty draft and a highlighted item now removes it, a keyboard-driven delete alongside the existing hover "x" and context menu "Remove".
| // checklist) — independent of whether a status glyph is shown, | ||
| // since workspaces now default to hidden status and must still | ||
| // be able to add checklist items from that state. | ||
| if !workspaceSnapshot.checklistItems.isEmpty || checklistAddFieldActivationToken > 0 { |
There was a problem hiding this comment.
Activation Token Persists When an empty checklist is opened through the popover and then dismissed without adding an item, the add-field activation token is still left in
checklistAddFieldActivationTokens. The popover close path clears checklistPopoverWorkspaceId, but it does not consume the token. Since this gate keeps rendering the checklist section whenever checklistAddFieldActivationToken > 0, an otherwise-empty workspace can keep an empty checklist section alive after the user clicks outside the transient popover or cancels without committing. Clear the token when the popover add flow closes without a commit, or stop using the pending activation token as section visibility state after dismissal.
An empty VStack collapses to a degenerate frame, which broke the NSPopover anchor view's bounds/window attachment when a workspace had zero checklist items — "Add Checklist Item…" silently failed to open the popover. Keep a minimal invisible placeholder so the anchor always has real geometry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| if WorkspaceTodoFeature.checklistStyle == .popover { | ||
| statusPopoverWorkspaceId = nil | ||
| checklistPopoverWorkspaceId = workspaceId |
There was a problem hiding this comment.
Activation Token Persists This branch opens the checklist popover and leaves
checklistAddFieldActivationTokens set. If the user dismisses the transient popover without adding an item, the close path clears checklistPopoverWorkspaceId but does not consume the activation token. Since the sidebar section still renders when checklistAddFieldActivationToken > 0, an empty workspace can keep stale add state after the popover closes. Clear the token when the popover add flow is dismissed without a commit, or make section visibility stop depending on a token after that flow has closed.
…line alignment The prior zero-item placeholder fix didn't retry reliably: it depended on an unrelated later re-render landing after the anchor NSView actually attached to its window, so "Add Checklist Item..." on a fresh workspace usually opened nothing until some other UI event fired. Replace it with an AnchorView subclass that retries present() from viewDidMoveToWindow(), so the popover opens deterministically as soon as AppKit finishes attaching it. The popover host's NSViewRepresentable anchor was also stacked as a .background() spanning the whole checklist section, which suppressed .onHover for the item rows underneath it (the hover-reveal delete "x" rarely appeared). Shrink the anchor to a fixed 1x1pt .overlay corner so it never intercepts hover/hit-testing, and drop the now-unnecessary Color.clear placeholder that caused the "bottom padding" artifact. Both the inline expansion and the popover now show all items in a ScrollView capped at 6 rows instead of hard-clamping with a "... N more" row, and item rows use .firstTextBaseline alignment with an alignmentGuide offset (derived from font ascender/descender) so the checkbox and delete button center on the first line of wrapped multi-line item text rather than the baseline or the whole block. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit aee3354. Configure here.
WorkspaceTodoPaneItemRow was missing the same multi-line-wrap fix already applied to the sidebar's inline list and popover: long item text overflowed unwrapped past the pane's edge instead of wrapping. Adds .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.leading) to the item Text, and centers the checkbox on the text's first line via .alignmentGuide(.firstTextBaseline).
…resent churn Three root causes, each verified against a live tagged build: - Zero-item anchor: the section VStack renders no children for an empty checklist in popover style, so .overlay(.topTrailing) collapsed to the row's LEFT edge. The container now claims the row's full width (maxWidth: .infinity) so the anchor always sits at the real right edge. - Hover-reveal delete x: per-row .onContinuousHover state died whenever popover content was recreated or rows reflowed under a stationary pointer (no new mouse-moved event arrives). hoveredItemId is now DERIVED from one container-level pointer location plus row frames collected via preference, seeded from NSEvent.mouseLocation at window attach, so the x survives model changes and fresh presents. - Re-present churn: when AppKit closed the transient popover (app deactivation, click-away) the container's isPresented=false write landed asynchronously, so a stale re-render tick re-presented the popover the user just dismissed (observed live: five didShows in 18s). The host coordinator now latches after an AppKit-side close until the container acknowledges false; an explicit add-request token change clears the latch so "Add Checklist Item…" can always present. Also: programmatic/keyboard workspace switches now dismiss open checklist/status popovers (no outside click for transient behavior to catch), and the Todo Pane keeps arrow-key scroll-follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ifiers Independent artifact verification of the previous commit caught the delete x vanishing on a 1px pointer move right after a checklist mutation: SwiftUI's .onContinuousHover rebuilds its NSTrackingArea on content updates, and the first mouse event after a rebuild can arrive as a spurious .ended from the torn-down area with no follow-up .active until the next event. The popover's pointer location is now tracked by a persistent AppKit NSView (PopoverPointerTracker) whose NSTrackingArea only changes with geometry, never content: mouseEntered/mouseMoved report the location, mouseExited clears it, and viewDidMoveToWindow seeds from NSEvent.mouseLocation so a popover presenting under a resting pointer still shows hover affordances. .activeAlways keeps hover alive when the terminal pane steals key status from the popover window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| if WorkspaceTodoFeature.checklistStyle == .popover { | ||
| statusPopoverWorkspaceId = nil | ||
| checklistPopoverWorkspaceId = workspaceId | ||
| } else { |
There was a problem hiding this comment.
Activation Token Persists This add-request path still bumps
checklistAddFieldActivationTokens before opening the popover, but the popover can close without consuming that token. When a user opens the first-item popover and then presses Escape, clicks away, deactivates the app, or switches workspaces, the close path clears checklistPopoverWorkspaceId while the positive token still satisfies the empty-section visibility gate. That leaves the workspace in stale add state after the user canceled the add flow. Clear the token from the popover cancel/dismiss path when no item is committed.
…n files Keeps SidebarWorkspaceChecklistPopover.swift and SidebarWorkspaceChecklistView.swift under the 500-line tracking threshold of the Swift file-length budget gate. Pure moves: PopoverPointerTracker + ChecklistPopoverRowFramesKey to ChecklistPopoverPointerTracking.swift, ChecklistSummaryPopoverModifier to ChecklistSummaryPopoverModifier.swift (private -> internal). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the popover opening by itself while typing in the todo pane (user video 2026-07-13): the popover host coordinator captured its isPresented @binding once at creation. The binding's get closure closes over that render's value snapshot, so a coordinator created while the popover was hidden read a frozen `false` forever. On an AppKit-side close (click-away), popoverDidClose's `if isPresented` guard read that stale false and skipped writing false back, leaving the container's checklistPopoverWorkspaceId stuck set — and every later model change (e.g. committing an item in the todo pane) re-rendered the host with isPresented=true and re-presented the popover with no user action. The coordinator now holds a Binding<Bool> refreshed on every updateNSView tick, so reads and write-backs always target current container state. Reproduced deterministically before the fix (todo add to the selected workspace -> didShow ~1s later, zero user actions). Open as Pane now focuses an already-open todo pane usefully: the footer closes the popover BEFORE opening the pane (NSPopover teardown restores the previous first responder, which clobbered the focus openPane had just set), and openOrFocusWorkspaceTodoSurface bumps a new addFieldArmToken on WorkspaceTodoPanel so the pane re-arms its add field even when it was already focused and isFocused never transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The external-dismissal latch treated ANY presentationRequestToken change as a fresh present request, but the container RESETS the token to zero after the add field arms (consumption) — that reset unlatched a popover the user had just dismissed and let a stale re-render tick re-present it. Token zero now means consumed, not requested. Also logs checklist popover container-state changes (DEBUG) for lifecycle forensics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review finding (Greptile P1 / Cursor): dismissing the first-item popover without committing (Escape, click-away, app deactivation, workspace switch) cleared checklistPopoverWorkspaceId but left the add-field activation token set, leaving the workspace in stale "add requested" state and keeping the empty section mounted invisibly. Any false write through the popover-presented binding now also consumes the activation, and the workspace-switch dismissal path clears the dismissed workspace's token directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review finding (Codex P1): present() re-enters on every parent update tick while the popover is shown, and the root layoutSubtreeIfNeeded() added for same-transaction first presents ran before the isShown guard — synchronously flushing the entire main window's layout on every checklist/title/status change while any todo popover was open. The root layout, identity bump, and initial sizing now all sit behind the isShown guard; shown-popover content and size updates flow through update(model:) -> refreshContent() as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
…lesced hover Three review findings on the checklist popover: - The empty-section mount condition now includes the popover-presented state: unmounting on token consumption dismantled the popover's anchor mid-presentation, so Return on an empty first-item draft or deleting the last item tore the popover down (and the first add could race the snapshot refresh). - PopoverPointerTracker's view returns nil from hitTest: tracking areas fire from geometry alone, and the full-size background view must never win clicks over the SwiftUI controls it sits behind. - Pointer location now lives in a reference box mutated per mouse event; SwiftUI state (hoveredItemId) is written only when the hovered row actually changes, so per-pixel mouse movement no longer rebuilds every popover row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a43c99c
into
restore-workspace-todos-default-none
…7790) * Restore workspaces-as-todos, default new workspaces to None status Reverts the revert (PR #7761) of "Workspaces as todos: inferred status lifecycle + per-workspace checklist" (#7216), restoring the feature. Additionally flips WorkspaceTodoState.statusHidden's default to true so newly created workspaces start with the status glyph hidden (the existing "None" opt-out state) instead of Auto — the feature is opt-in per workspace going forward. Pre-existing persisted workspaces that predate this field still restore to their historical visible/Auto state. * Route the checklist's first item through the popover in popover style (#7797) * Route the checklist's first item through the popover in popover style usesPopoverPresentation && totalCount > 0 forced inline entry for a workspace's very first checklist item regardless of the popover setting, since an empty checklist has no summary line to anchor a popover to. Move the popover anchor from the summary line to the section container (stable across the 0->1 item transition), route the ghost "Add item" button through the same popover-toggle path as the summary line, and fix the "Add Checklist Item..." notification handler in ContentView.swift (it special-cased non-empty checklists, so the context-menu/palette path still fell back to inline for a fresh workspace even after the view-level fix). Inline style is unaffected. * Fix: checklist section never rendered for hidden-status workspaces taskStatus is nil whenever a workspace's status is hidden (statusHidden), which restore-workspace-todos-default-none made the default for all new workspaces. The checklist section's render gate required taskStatus != nil, so "Add Checklist Item..." silently did nothing for any new workspace. Drop that condition; the checklist section only needs its own content/token gate. * Add hover-reveal delete button to checklist item rows The only way to delete a checklist item was a hidden right-click "Remove" context menu entry. Add a trailing xmark.circle.fill button that appears on row hover (reserved layout space, opacity/hit-test toggle only, so row height never jumps) in both row implementations: the inline sidebar row and the NSPopover-hosted row. Both call the existing actions.removeItem(_:) closure, the same one the context menu already used; the context menu entry is unchanged. New localized tooltip key sidebar.checklist.removeItemTooltip ("Remove item" / "項目を削除"). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix checklist popover focus ambiguity, ghost row, and item alignment; add keyboard delete - Highlighting a checklist item (click, Up/Down) and typing a new item in the add field are now mutually exclusive: a non-empty draft clears any highlight, and highlight-setting paths no-op while a draft exists. Makes Return's outcome (toggle item vs. commit new item) unambiguous. - Popover-style checklists no longer show a persistent ghost "Add item" row inline when a workspace has zero items. "Add Checklist Item…" now opens the popover directly with nothing shown inline until an item is actually added, at which point the existing summary line is the small status preview. - Item rows now center-align the checkbox and text (was firstTextBaseline, which misaligned the checkbox glyph against the text's baseline). - Backspace with an empty draft and a highlighted item now removes it, a keyboard-driven delete alongside the existing hover "x" and context menu "Remove". * Fix checklist popover anchor for zero-item workspaces An empty VStack collapses to a degenerate frame, which broke the NSPopover anchor view's bounds/window attachment when a workspace had zero checklist items — "Add Checklist Item…" silently failed to open the popover. Keep a minimal invisible placeholder so the anchor always has real geometry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix checklist popover open reliability, hover, scroll cap, and multi-line alignment The prior zero-item placeholder fix didn't retry reliably: it depended on an unrelated later re-render landing after the anchor NSView actually attached to its window, so "Add Checklist Item..." on a fresh workspace usually opened nothing until some other UI event fired. Replace it with an AnchorView subclass that retries present() from viewDidMoveToWindow(), so the popover opens deterministically as soon as AppKit finishes attaching it. The popover host's NSViewRepresentable anchor was also stacked as a .background() spanning the whole checklist section, which suppressed .onHover for the item rows underneath it (the hover-reveal delete "x" rarely appeared). Shrink the anchor to a fixed 1x1pt .overlay corner so it never intercepts hover/hit-testing, and drop the now-unnecessary Color.clear placeholder that caused the "bottom padding" artifact. Both the inline expansion and the popover now show all items in a ScrollView capped at 6 rows instead of hard-clamping with a "... N more" row, and item rows use .firstTextBaseline alignment with an alignmentGuide offset (derived from font ascender/descender) so the checkbox and delete button center on the first line of wrapped multi-line item text rather than the baseline or the whole block. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix checklist item text wrap in the dedicated Todo Pane view WorkspaceTodoPaneItemRow was missing the same multi-line-wrap fix already applied to the sidebar's inline list and popover: long item text overflowed unwrapped past the pane's edge instead of wrapping. Adds .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.leading) to the item Text, and centers the checkbox on the text's first line via .alignmentGuide(.firstTextBaseline). * Fix checklist popover zero-item anchor, hover-reveal delete, and re-present churn Three root causes, each verified against a live tagged build: - Zero-item anchor: the section VStack renders no children for an empty checklist in popover style, so .overlay(.topTrailing) collapsed to the row's LEFT edge. The container now claims the row's full width (maxWidth: .infinity) so the anchor always sits at the real right edge. - Hover-reveal delete x: per-row .onContinuousHover state died whenever popover content was recreated or rows reflowed under a stationary pointer (no new mouse-moved event arrives). hoveredItemId is now DERIVED from one container-level pointer location plus row frames collected via preference, seeded from NSEvent.mouseLocation at window attach, so the x survives model changes and fresh presents. - Re-present churn: when AppKit closed the transient popover (app deactivation, click-away) the container's isPresented=false write landed asynchronously, so a stale re-render tick re-presented the popover the user just dismissed (observed live: five didShows in 18s). The host coordinator now latches after an AppKit-side close until the container acknowledges false; an explicit add-request token change clears the latch so "Add Checklist Item…" can always present. Also: programmatic/keyboard workspace switches now dismiss open checklist/status popovers (no outside click for transient behavior to catch), and the Todo Pane keeps arrow-key scroll-follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Own checklist popover hover tracking in AppKit, not SwiftUI hover modifiers Independent artifact verification of the previous commit caught the delete x vanishing on a 1px pointer move right after a checklist mutation: SwiftUI's .onContinuousHover rebuilds its NSTrackingArea on content updates, and the first mouse event after a rebuild can arrive as a spurious .ended from the torn-down area with no follow-up .active until the next event. The popover's pointer location is now tracked by a persistent AppKit NSView (PopoverPointerTracker) whose NSTrackingArea only changes with geometry, never content: mouseEntered/mouseMoved report the location, mouseExited clears it, and viewDidMoveToWindow seeds from NSEvent.mouseLocation so a popover presenting under a resting pointer still shows hover affordances. .activeAlways keeps hover alive when the terminal pane steals key status from the popover window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Split checklist popover pointer tracking and summary modifier into own files Keeps SidebarWorkspaceChecklistPopover.swift and SidebarWorkspaceChecklistView.swift under the 500-line tracking threshold of the Swift file-length budget gate. Pure moves: PopoverPointerTracker + ChecklistPopoverRowFramesKey to ChecklistPopoverPointerTracking.swift, ChecklistSummaryPopoverModifier to ChecklistSummaryPopoverModifier.swift (private -> internal). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Ratchet ContentView.swift file-length budget down to actual (16262) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Trim ContentView comment growth to net zero for the file-length hard cap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix spontaneous checklist popover re-presents and Open as Pane focus Root cause of the popover opening by itself while typing in the todo pane (user video 2026-07-13): the popover host coordinator captured its isPresented @binding once at creation. The binding's get closure closes over that render's value snapshot, so a coordinator created while the popover was hidden read a frozen `false` forever. On an AppKit-side close (click-away), popoverDidClose's `if isPresented` guard read that stale false and skipped writing false back, leaving the container's checklistPopoverWorkspaceId stuck set — and every later model change (e.g. committing an item in the todo pane) re-rendered the host with isPresented=true and re-presented the popover with no user action. The coordinator now holds a Binding<Bool> refreshed on every updateNSView tick, so reads and write-backs always target current container state. Reproduced deterministically before the fix (todo add to the selected workspace -> didShow ~1s later, zero user actions). Open as Pane now focuses an already-open todo pane usefully: the footer closes the popover BEFORE opening the pane (NSPopover teardown restores the previous first responder, which clobbered the focus openPane had just set), and openOrFocusWorkspaceTodoSurface bumps a new addFieldArmToken on WorkspaceTodoPanel so the pane re-arms its add field even when it was already focused and isFocused never transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Only nonzero activation tokens unlatch the popover's dismissal ack The external-dismissal latch treated ANY presentationRequestToken change as a fresh present request, but the container RESETS the token to zero after the add field arms (consumption) — that reset unlatched a popover the user had just dismissed and let a stale re-render tick re-present it. Token zero now means consumed, not requested. Also logs checklist popover container-state changes (DEBUG) for lifecycle forensics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Consume pending add-field activation when the checklist popover closes Review finding (Greptile P1 / Cursor): dismissing the first-item popover without committing (Escape, click-away, app deactivation, workspace switch) cleared checklistPopoverWorkspaceId but left the add-field activation token set, leaving the workspace in stale "add requested" state and keeping the empty section mounted invisibly. Any false write through the popover-presented binding now also consumes the activation, and the workspace-switch dismissal path clears the dismissed workspace's token directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Run the popover's window-wide pre-show layout only on hidden-to-shown Review finding (Codex P1): present() re-enters on every parent update tick while the popover is shown, and the root layoutSubtreeIfNeeded() added for same-transaction first presents ran before the isShown guard — synchronously flushing the entire main window's layout on every checklist/title/status change while any todo popover was open. The root layout, identity bump, and initial sizing now all sit behind the isShown guard; shown-popover content and size updates flow through update(model:) -> refreshContent() as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review: keep popover host mounted, hit-test pass-through, coalesced hover Three review findings on the checklist popover: - The empty-section mount condition now includes the popover-presented state: unmounting on token consumption dismantled the popover's anchor mid-presentation, so Return on an empty first-item draft or deleting the last item tore the popover down (and the first add could race the snapshot refresh). - PopoverPointerTracker's view returns nil from hitTest: tracking areas fire from geometry alone, and the full-size background view must never win clicks over the SwiftUI controls it sits behind. - Pointer location now lives in a reference box mutated per mouse event; SwiftUI state (hoveredItemId) is written only when the hovered row actually changes, so per-pixel mouse movement no longer rebuilds every popover row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> * Reconcile Swift file-length budget after merging main Union of both sides' entries kept stale pre-merge numbers; ratchet each flagged entry to the merged tree's actual length and track the two files the merge pushed over the 500-line threshold. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fit todos-restore growth inside the Swift file-length hard cap The file-length gate forbids any net growth in >900-line files and any file newly crossing the 500-line threshold. The restore's additions to seven such files are irreducible call-site/persistence code, so offset them by folding this branch's own added blocks into the files' existing dense style and tightening verbose doc comments (content and links preserved). Pure line folds; no behavior, ordering, or declaration changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Deduplicate SidebarWorkspaceRowInteractionStateTests after merge The branch had extracted the suite to its own file while main updated the in-file copy (hover-lifecycle hang fix removed tests asserting the old force-publish behavior). Keep the branch's file layout with main's current suite content, one definition total; the sharding guard fails on duplicate selector identifiers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix missing newline at test-suite splice Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com>

Stacks on #7790
This PR is stacked on #7790 (
restore-workspace-todos-default-none) and should be reviewed/merged after it. Do not merge this PR on its own.Summary
SidebarWorkspaceChecklistView.swift'spresentsPopoverwasusesPopoverPresentation && totalCount > 0, so a workspace with zero checklist items always fell into inline text-field entry regardless ofsidebar.beta.workspaceTodos.checklistStyle(default.popover). A workspace's very first checklist item never got the popover treatment.presentsPopovernow equalsusesPopoverPresentationdirectly (nototalCountgate).ChecklistSummaryPopoverModifier(the NSPopover anchor) fromsummaryLineto the section's outer container, so the same backingNSView/Coordinatoranchors the popover across the 0->1 item transition instead of re-anchoring to a freshly created view (which would close and immediately reopen the popover).presentsPopoveris true.ContentView.swift's.onReceivehandler for.workspaceChecklistAddItemRequested: it special-cased!workspace.todoState.checklist.isEmptybefore routing to the popover, so the "Add Checklist Item..." context-menu/palette action still fell back to inline for a workspace's first item even after the view-level fix. Removed that gate; popover style now always routes the add request into the popover..inlinestyle is untouched:presentsPopoverisfalsethere regardless of item count, same as before.No changes to
WorkspaceTodoChecklistStyle, its default, the settings UI, orWorkspaceTodoState.statusHidden/default-status logic (all handled by #7790).Test plan
No new pure-logic seam was created worth a unit test:
presentsPopoverreduces to exactlyusesPopoverPresentation, an identity, and the rest of the fix is SwiftUI view-wiring (anchor attachment point, button action branch, notification-handler condition) that isn't expressible as a standalone testable function. Verified manually instead, tagchkpop:.popoverstyle (default): triggering "Add Checklist Item..." (context-menu/palette path, exercised via thedebug.workspace_todo.checklist_add_fielddebug RPC, which shares the sameWorkspaceTodoActions.requestChecklistAddFieldpath as the context menu and the ghost row's own tap) opens the anchoredNSPopoverimmediately, with the header showing the workspace title and0/0, and the add field focused — no intermediate inlineTextFieldstate at any point. Screenshot evidence captured.1. [ ] Buy milk,0/1 completed), popover stays anchored (no visible close/reopen), summary line updates to0/1 · Buy milk.2. [ ] Walk the dog,0/2 completed) — pre-existing popover-for-non-empty-checklist behavior unaffected.sidebarWorkspaceTodosChecklistStyleoverridden toinline: triggering the same add-item path shows the inline ghost-rowTextField(not a popover) for the very first item — confirms no regression to the non-default path../scripts/reload.sh --tag chkpopsucceeded (local reload;reload-cloud.shknown-broken in this hq checkout).cmux DEV chkpopprocesses remain.🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Changes are mostly sidebar/todo UI and NSPopover focus/hover plumbing, but they touch lazy-list layout, popover lifecycle, and keyboard routing in
ContentView, so regressions could show up as wrong anchor position, popover churn, or focus stealing from the terminal.Overview
Popover style now covers empty checklists and the first item.
presentsPopoverno longer depends on item count; the NSPopover anchor lives on the section container via a full-widthChecklistSummaryPopoverModifier(1×1pt trailing overlay) so the sameNSViewsurvives 0→1 items.ContentViewalways routes.workspaceChecklistAddItemRequestedto the popover in popover style, dismisses checklist/status popovers on workspace switch, and shows the checklist block without requiring a visible status glyph.SidebarWorkspaceTodoPopoverHostis hardened against real-world AppKit/SwiftUI races:AnchorViewretriespresent()after window attach; missing window no longer forcesisPresented = false; anawaitingDismissAcklatch stops close/reopen churn after transient dismissal;presentationRequestToken(add-field bump) clears the latch; layout is forced from the window root beforeshow.Checklist UI behavior is expanded and polished across sidebar, popover, and todo pane. The “… N more” clamp is replaced by scrollable lists capped at six rows. Long item text wraps with first-line-aligned checkboxes and controls. Hover-reveal delete buttons (plus
sidebar.checklist.removeItemTooltip) appear on inline rows (onContinuousHover) and in the popover via newPopoverPointerTracker(persistent AppKit tracking + geometry-derived hover, withPopoverKeyWindowElevator). The popover add field gains empty-draft Backspace-to-delete and stricter highlight vs. typing disambiguation; the pane scrolls keyboard highlight into view.Reviewed by Cursor Bugbot for commit cf219bf. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Empty checklists in
.popoverstyle now open the checklist popover for the first item reliably, with a stable right‑edge anchor, robust hover/focus, and no inline ghost row..inlineis unchanged.Bug Fixes
.popover(nototalCountgate) and route.workspaceChecklistAddItemRequested; remove the inline ghost row when empty.AnchorViewthat retries after window attach; stays right‑aligned, doesn’t intercept row hover, and remains mounted while the popover is open to avoid teardown during first‑add/last‑delete.PopoverPointerTracker(hit‑test pass‑through) with pointer location held in a reference box and row frames via preference; coalesces updates, survives content changes, and is kept alive by a key‑window elevator.isPresentedbinding each update, latching after AppKit closes, only unlatching on non‑zero add‑request tokens; run root layout only on hidden‑to‑shown before show. Consume pending add‑field activation on close; workspace switches dismiss popovers and clear tokens.New Features
sidebar.checklist.removeItemTooltip(en, ja).Written for commit 73fa1c1. Summary will update on new commits.