feat(bulk): replace modals with anchored flyouts#38
Conversation
Bulk edit, rename, reorder, mark, and random-assign now open in a shared BulkFlyout shell with Primer tokens and flat surfaces instead of full-screen modals. The actions bar and menu are reorganized with keyboard chords and updated queue/transfer handling in the background worker.
There was a problem hiding this comment.
Cursor auto review
Found 3 actionable issue(s) on changed lines.
Large, well-structured flyout migration with solid transfer pre-flight and bulk-close undo wiring. Actionable: the new Mark Unlock path calls bulkLock (lock mutation) and there is no unlock handler/mutation; reorder/edit flyout async loads also lack in-flight cancellation and can apply stale data after close or fast input.
Generated automatically when this PR was submitted using Cursor CLI with --model auto.
There was a problem hiding this comment.
20 issues found across 46 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/features/bulk-random-assign-flyout.tsx">
<violation number="1" location="src/features/bulk-random-assign-flyout.tsx:319">
P2: The "Preserve existing assignees" checkbox is currently a no-op: its state is never used when applying random assignment.</violation>
</file>
<file name="src/features/bulk-transfer-modal.tsx">
<violation number="1" location="src/features/bulk-transfer-modal.tsx:137">
P2: When merging recents, prefer the current search result object for matching repo IDs so eligibility/disabled state stays accurate.</violation>
<violation number="2" location="src/features/bulk-transfer-modal.tsx:206">
P2: Reset `eligibilityLoading` on early-return paths in the preflight effect; otherwise the transfer button can remain stuck in a disabled "Checking…" state after target switches.</violation>
</file>
<file name="src/features/bulk-edit-flyout.tsx">
<violation number="1" location="src/features/bulk-edit-flyout.tsx:110">
P1: Handle bulkUpdate dispatch failures before marking the field applied and closing the flyout.</violation>
<violation number="2" location="src/features/bulk-edit-flyout.tsx:141">
P2: Guard async metadata responses so older searches cannot overwrite newer results.</violation>
<violation number="3" location="src/features/bulk-edit-flyout.tsx:257">
P2: This flow skips the documented “Review & Apply” step and allows immediate mutation from the value pane.</violation>
</file>
<file name="src/ui/bulk-flyout.tsx">
<violation number="1" location="src/ui/bulk-flyout.tsx:207">
P2: `bodySx` is applied to the flyout shell instead of the body container, which violates the prop contract and can cause unintended layout overrides.</violation>
</file>
<file name="src/features/keyboard-help-overlay.tsx">
<violation number="1" location="src/features/keyboard-help-overlay.tsx:114">
P3: The bar-chord row rendering duplicates the layout pattern from the shortcuts section below. Consider extracting a shared `ShortcutRow` sub-component to keep the styling in one place.</violation>
</file>
<file name="src/features/bulk-rename-flyout.tsx">
<violation number="1" location="src/features/bulk-rename-flyout.tsx:84">
P2: Guard `getItemTitles` responses against stale async completions; older requests can overwrite newer flyout state after close/reopen or selection changes.</violation>
</file>
<file name="src/features/bulk-rename-utils.ts">
<violation number="1" location="src/features/bulk-rename-utils.ts:112">
P2: `{date}` token uses UTC (`toISOString`) instead of local calendar date, which can produce the wrong day for users outside UTC.</violation>
</file>
<file name="src/features/bulk-edit-flyout-helpers.ts">
<violation number="1" location="src/features/bulk-edit-flyout-helpers.ts:47">
P2: Number validation should reject non-finite values; `Infinity` currently passes and can be submitted as a bulk field update.</violation>
</file>
<file name="src/features/bulk-actions-menu.tsx">
<violation number="1" location="src/features/bulk-actions-menu.tsx:165">
P2: The Delete menu item always shows `D`, but for single selection `D` is mapped to Duplicate, so this shortcut hint is incorrect and misleading.</violation>
</file>
<file name="src/features/bulk-mark-flyout.tsx">
<violation number="1" location="src/features/bulk-mark-flyout.tsx:210">
P1: The new `unlock` flyout option dispatches the lock mutation path, so “Unlock conversations” performs a lock operation instead of unlocking.</violation>
</file>
<file name="src/background/bulk-state.ts">
<violation number="1" location="src/background/bulk-state.ts:94">
P2: Undo targets are inferred as “not failed”, which incorrectly includes unprocessed items after cancellation. This can show an Undo action for items that were never closed.</violation>
</file>
<file name="src/lib/project-table-dom.ts">
<violation number="1" location="src/lib/project-table-dom.ts:143">
P2: This no-op prevents pin/lock uncertainty from being reflected in `unknownCount`, so the mark flyout fallback can be skipped incorrectly.</violation>
</file>
<file name="src/features/bulk-reorder-flyout.tsx">
<violation number="1" location="src/features/bulk-reorder-flyout.tsx:88">
P2: The reorder-context fetch has an out-of-order response race; stale async results can overwrite newer state and show/apply the wrong target list.</violation>
<violation number="2" location="src/features/bulk-reorder-flyout.tsx:174">
P2: Top/Bottom/Custom actions should be disabled when reorder context failed or is unresolved; currently they can dispatch a no-op reorder after fetch errors.</violation>
</file>
<file name="src/lib/queue-store.ts">
<violation number="1" location="src/lib/queue-store.ts:285">
P1: Auto-dismissed processes leave `phaseHintsMap` entries behind, which can leak stale undo/retry hints into future runs with the same `processId`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Wrap resolveProjectItemIdsWithTitles in validateTransferEligibility with try/catch so transient GraphQL errors yield per-item unresolved rows instead of rejecting the whole message. Extract classification into transfer-eligibility helpers and add unit tests.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
There was a problem hiding this comment.
1 issue found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/background/bulk-state.ts">
<violation number="1" location="src/background/bulk-state.ts:94">
P2: Undo targets are inferred as “not failed”, which incorrectly includes unprocessed items after cancellation. This can show an Undo action for items that were never closed.</violation>
<violation number="2" location="src/background/bulk-state.ts:327">
P1: The `bulkUnlock` message handler is registered twice, so each unlock request can run twice and emit duplicate queue updates.</violation>
</file>
<file name="src/features/bulk-random-assign-flyout.tsx">
<violation number="1" location="src/features/bulk-random-assign-flyout.tsx:319">
P2: The "Preserve existing assignees" checkbox is currently a no-op: its state is never used when applying random assignment.</violation>
</file>
<file name="src/features/bulk-transfer-modal.tsx">
<violation number="1" location="src/features/bulk-transfer-modal.tsx:137">
P2: When merging recents, prefer the current search result object for matching repo IDs so eligibility/disabled state stays accurate.</violation>
<violation number="2" location="src/features/bulk-transfer-modal.tsx:206">
P2: Reset `eligibilityLoading` on early-return paths in the preflight effect; otherwise the transfer button can remain stuck in a disabled "Checking…" state after target switches.</violation>
</file>
<file name="src/features/bulk-edit-flyout.tsx">
<violation number="1" location="src/features/bulk-edit-flyout.tsx:141">
P2: Guard async metadata responses so older searches cannot overwrite newer results.</violation>
<violation number="2" location="src/features/bulk-edit-flyout.tsx:257">
P2: This flow skips the documented “Review & Apply” step and allows immediate mutation from the value pane.</violation>
</file>
<file name="src/features/keyboard-help-overlay.tsx">
<violation number="1" location="src/features/keyboard-help-overlay.tsx:114">
P3: The bar-chord row rendering duplicates the layout pattern from the shortcuts section below. Consider extracting a shared `ShortcutRow` sub-component to keep the styling in one place.</violation>
</file>
<file name="src/features/bulk-rename-flyout.tsx">
<violation number="1" location="src/features/bulk-rename-flyout.tsx:84">
P2: Guard `getItemTitles` responses against stale async completions; older requests can overwrite newer flyout state after close/reopen or selection changes.</violation>
</file>
<file name="src/features/bulk-rename-utils.ts">
<violation number="1" location="src/features/bulk-rename-utils.ts:112">
P2: `{date}` token uses UTC (`toISOString`) instead of local calendar date, which can produce the wrong day for users outside UTC.</violation>
</file>
<file name="src/features/bulk-edit-flyout-helpers.ts">
<violation number="1" location="src/features/bulk-edit-flyout-helpers.ts:47">
P2: Number validation should reject non-finite values; `Infinity` currently passes and can be submitted as a bulk field update.</violation>
</file>
<file name="src/features/bulk-actions-menu.tsx">
<violation number="1" location="src/features/bulk-actions-menu.tsx:165">
P2: The Delete menu item always shows `D`, but for single selection `D` is mapped to Duplicate, so this shortcut hint is incorrect and misleading.</violation>
</file>
<file name="src/features/bulk-mark-flyout.tsx">
<violation number="1" location="src/features/bulk-mark-flyout.tsx:210">
P1: The new `unlock` flyout option dispatches the lock mutation path, so “Unlock conversations” performs a lock operation instead of unlocking.</violation>
</file>
<file name="src/lib/project-table-dom.ts">
<violation number="1" location="src/lib/project-table-dom.ts:143">
P2: This no-op prevents pin/lock uncertainty from being reflected in `unknownCount`, so the mark flyout fallback can be skipped incorrectly.</violation>
</file>
<file name="src/features/bulk-reorder-flyout.tsx">
<violation number="1" location="src/features/bulk-reorder-flyout.tsx:88">
P2: The reorder-context fetch has an out-of-order response race; stale async results can overwrite newer state and show/apply the wrong target list.</violation>
<violation number="2" location="src/features/bulk-reorder-flyout.tsx:174">
P2: Top/Bottom/Custom actions should be disabled when reorder context failed or is unresolved; currently they can dispatch a no-op reorder after fetch errors.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
There was a problem hiding this comment.
1 issue found across 11 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/background/bulk-update.ts">
<violation number="1" location="src/background/bulk-update.ts:28">
P3: The bulkUpdate payload type is duplicated locally instead of reusing the shared message contract, which increases the chance of type drift between sender and background handler.</violation>
</file>
<file name="src/features/bulk-transfer-modal.tsx">
<violation number="1" location="src/features/bulk-transfer-modal.tsx:137">
P2: When merging recents, prefer the current search result object for matching repo IDs so eligibility/disabled state stays accurate.</violation>
<violation number="2" location="src/features/bulk-transfer-modal.tsx:206">
P2: Reset `eligibilityLoading` on early-return paths in the preflight effect; otherwise the transfer button can remain stuck in a disabled "Checking…" state after target switches.</violation>
</file>
<file name="src/features/bulk-edit-flyout.tsx">
<violation number="1" location="src/features/bulk-edit-flyout.tsx:141">
P2: Guard async metadata responses so older searches cannot overwrite newer results.</violation>
<violation number="2" location="src/features/bulk-edit-flyout.tsx:257">
P2: This flow skips the documented “Review & Apply” step and allows immediate mutation from the value pane.</violation>
</file>
<file name="src/features/keyboard-help-overlay.tsx">
<violation number="1" location="src/features/keyboard-help-overlay.tsx:114">
P3: The bar-chord row rendering duplicates the layout pattern from the shortcuts section below. Consider extracting a shared `ShortcutRow` sub-component to keep the styling in one place.</violation>
</file>
<file name="src/features/bulk-edit-flyout-helpers.ts">
<violation number="1" location="src/features/bulk-edit-flyout-helpers.ts:47">
P2: Number validation should reject non-finite values; `Infinity` currently passes and can be submitted as a bulk field update.</violation>
</file>
<file name="src/features/bulk-actions-menu.tsx">
<violation number="1" location="src/features/bulk-actions-menu.tsx:165">
P2: The Delete menu item always shows `D`, but for single selection `D` is mapped to Duplicate, so this shortcut hint is incorrect and misleading.</violation>
</file>
<file name="src/features/bulk-mark-flyout.tsx">
<violation number="1" location="src/features/bulk-mark-flyout.tsx:210">
P1: The new `unlock` flyout option dispatches the lock mutation path, so “Unlock conversations” performs a lock operation instead of unlocking.</violation>
</file>
<file name="src/background/bulk-state.ts">
<violation number="1" location="src/background/bulk-state.ts:94">
P2: Undo targets are inferred as “not failed”, which incorrectly includes unprocessed items after cancellation. This can show an Undo action for items that were never closed.</violation>
</file>
<file name="src/lib/project-table-dom.ts">
<violation number="1" location="src/lib/project-table-dom.ts:143">
P2: This no-op prevents pin/lock uncertainty from being reflected in `unknownCount`, so the mark flyout fallback can be skipped incorrectly.</violation>
</file>
<file name="src/features/bulk-reorder-flyout.tsx">
<violation number="1" location="src/features/bulk-reorder-flyout.tsx:88">
P2: The reorder-context fetch has an out-of-order response race; stale async results can overwrite newer state and show/apply the wrong target list.</violation>
<violation number="2" location="src/features/bulk-reorder-flyout.tsx:174">
P2: Top/Bottom/Custom actions should be disabled when reorder context failed or is unresolved; currently they can dispatch a no-op reorder after fetch errors.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
toISOString() used UTC and could show the wrong day ahead of UTC. Format with local year/month/day and pin a Sydney timezone test.
Honor "preserve existing" in random assign by loading per-item assignees via getItemPreview and merging with strategy output. Guard rename title fetches against races, fix numbering RadioGroup labeling, and add tests.
Primer requires choice groups to expose a proper group label. Use RadioGroup.Label with the field name for single-select and iteration pickers.
There was a problem hiding this comment.
3 issues found across 9 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
- Reject non-finite numbers in bulk edit (Infinity no longer applies). - Reset eligibilityLoading on transfer-modal early-return paths. - Merge recent transfer destinations using current search items for fresh eligibility/disabled state. - Hide the duplicate D shortcut hint on Delete when count === 1. - Track lastCompleted in bulk close to exclude unprocessed items from the Undo target set after cancellation. - Add lockOrPinUnknownCount so pin/lock uncertainty no longer pollutes status fallback; mark flyout uses the new counter. - Guard async metadata responses in bulk edit, bulk reorder, and bulk rename to prevent stale results overwriting newer state. - Disable Top/Bottom/Custom reorder actions when context is loading, errored, or unresolved. - Refactor keyboard help overlay rows into a shared ShortcutRow. - Share BulkUpdateMessageData type between sender and background handler so the contract cannot drift. - Reset loadingExisting on random-assign early-return path. - Replace Date.now() rename fetch token with a monotonic counter and ensure cleanup releases the loading spinner.
Summary
BulkFlyoutshellChanges
Flyouts & shell (
src/ui/bulk-flyout.tsx,src/features/bulk-*-flyout.tsx)bulk-actions-modalsand the actions barBulk bar & menu (
src/features/bulk-actions-bar.tsx,bulk-actions-menu.tsx)use-bar-keyboard-chordsand document chords in the keyboard help overlayQueue & background (
src/lib/queue-store.ts,src/background/*)Tests
Test Plan
pnpm typecheckand targeted vitest forsrc/features/__tests__/bulk-*andsrc/ui/__tests__/bulk-flyout.test.tsxSummary by cubic
Replaced bulk-action modals with anchored, Primer-styled flyouts powered by a shared
BulkFlyout. The bulk bar now supports keyboard chords, and the queue tracker adds clearer phases with Undo/Retry.New Features
BulkFlyoutshell (simple/tabbed/drilldown) with flat Primer surfaces.Bug Fixes
bulkUpdatedispatch before closing, rejects non-finite numbers, and shows a clear queue-full message.bulkUnlock; addslockOrPinUnknownCountso unknowns don’t pollute status fallback.{date}uses local calendar; fetch guarded with a monotonic token and cleanup to prevent spinner leaks; fixed numbering radio labeling.BulkFlyoutappliesbodySxto the body container; Bulk Edit usesRadioGroup.Labelfor accessibility; sharedBulkUpdateMessageDatatype keeps sender and background in sync.count === 1.Written for commit 838c46a. Summary will update on new commits.