Skip to content

Refactor review-page: first-party reviewed state and decomposition into actions, DTOs, traits, and a JS module#101

Merged
fgilio merged 15 commits into
mainfrom
refactor/reviewed-state-first-party
Jun 16, 2026
Merged

Refactor review-page: first-party reviewed state and decomposition into actions, DTOs, traits, and a JS module#101
fgilio merged 15 commits into
mainfrom
refactor/reviewed-state-first-party

Conversation

@fgilio

@fgilio fgilio commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Closes:

  • none

Related:

Summary

Make the review page's reviewed-state server-owned (first-party), then decompose the page itself. ⚡review-page.blade.php was a ~2,900-line outlier holding state machines, comment writes, inline Alpine, and large markup blocks in one file. This PR relocates that logic into tested units while the page stays a single Livewire component.

Why

The page renders N diff-file children, so a parent re-render rehydrates them all (the 1+N hydration hazard). That rules out splitting its sections into nested components. The page can still shed weight by pushing pure decision/write logic into the app layer (Actions/DTOs), inline Alpine into a JS module, cohesive non-render clusters into traits, and large markup into partials, leaving a thin coordinator whose render contract reads in one place.

Solution

Keep one Livewire component as the event/render coordinator. Move everything that does not need the render pipeline out of the view file, and keep skipRender/renderIsland/dispatch glue on the coordinator.

Changes

  • Reviewed state, first-party: render the reviewed counter and Hide-reviewed toggle as server-owned Livewire islands, broadcast the authoritative mark so each diff-file converges, retire the Alpine reviewed-file mirror and the dead reviewedFileMap projection, and keep the counter island fresh on full renders.
  • Inline Alpine to a module: the page-root and change-poller Alpine (~185 lines) move to public/js/review-page.js (UMD, unit-tested with Vitest), including the pure remote-link menu logic.
  • Divergence decision to an action: ResolveDivergenceStateAction + DivergenceDecision DTO + DivergenceDecisionKind enum own the divergence decision tree; closures keep the aligned path query-free.
  • Comment writes to an action: ReviewCommentWorkflowAction handles add/draft/update/delete/clearAll/restore and returns a ReviewCommentMutation the page applies through applyCommentMutation(). No direct Comment writes remain in the view file.
  • Page internals to traits: App\Concerns\ReviewPage\{ReviewsBranchDivergence, ManagesReviewTrash, ExportsReview} hold the cohesive clusters; CountPersistedCommentsAction owns the divergence persisted-comment read so the Livewire-layer traits delegate model access to actions.
  • Markup to partials: the Settings dropdown and Trash list move to resources/views/pages/partials/ behind @include; islands and toolbar stay inline.
  • Arch: the DTO allowlist recognizes App\Concerns\ReviewPage as Livewire-layer code (DTOs allowed, model access still action-only).
  • Net: ⚡review-page.blade.php is now 1,723 lines, with the extracted logic covered by new action/DTO/JS/browser tests.

Testing

vendor/bin/pint --dirty --format agent     # pass
composer test:types                        # PHPStan level 6: 0 errors
composer test                              # Pest Core (Unit + Arch): 1502 passed
composer test:js                           # Vitest: 253 passed
composer test:browser                      # 193 passed, 1 skipped, 16 risky, 0 failed

Behavior-preserving throughout: trait methods moved verbatim, comment clear/restore map onto the existing mutation tail. Reviewed by a 7-angle code review and a dedicated removed-behavior audit (no correctness findings).

Deployment

  • Migrations: none
  • Cache clear: none (no diff cache version bump)
  • Monitor: review-page render, divergence banner (switch/keep-reviewing), comment clear-all + undo, trash discard/restore, settings dropdown (gitignore, base branch, external paths)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added branch divergence detection to notify when reviews drift from target branches
    • Enhanced comment workflow with undo support for add, update, delete, and bulk restore operations
    • Introduced discard and restore capability for working-tree file changes
    • Added review export and snapshot functionality
    • Added settings dropdown for configuring linked external paths and default base branch
  • Improvements

    • Optimized trash management with visual expiry countdown
    • Enhanced external change polling for real-time detection of modifications
    • Improved session persistence across reviewed-file state changes

fgilio and others added 12 commits June 15, 2026 21:25
…ed island

Moves the reviewed-progress counter and meter out of the Alpine reviewedCount mirror into a 'reviewed-summary' Livewire island. A mark/un-mark now re-renders just the island via renderIsland() on the latency path (hide-reviewed mode still does a full render since the toggle changes the visible file list). The counter and unmark paths share one settleReviewedRender() helper.

The @island lives in the page (Livewire) view and reaches the status strip through a $reviewedSummary slot, since the directive needs the Livewire view's scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The toggle's visibility moves off the Alpine reviewedCount mirror onto a same-named 'reviewed-summary' island, so renderIsland() flips it in step with the counter on a mark/un-mark. Removes the now-unused reviewedCount Alpine getter and adds browser coverage for the toggle appearing on the first mark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A full re-render (hide-reviewed mode, filtering) emits only a skip marker for islands, so the reviewed-summary counter went stale when a file was marked while hidden. Declaring the island always:true re-renders it inline on full renders, while renderIsland() still scopes the skip-render latency path.

Replaces a flaky browser probe (the lazy checkbox + hide re-render raced, so it never exercised hide mode) with a deterministic Livewire feature test asserting the counter reads 2/6 after a second mark in hide-reviewed mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e the Alpine mirror

The sidebar file list is now a file-list island (always:true). Its per-file reviewed button renders its state server-side instead of reading the page-root reviewedFiles Alpine object, and a mark/un-mark refreshes the island via renderIsland('file-list'). With the counter, Hide-reviewed toggle, and sidebar all server-owned, the page-root reviewedFiles mirror and its three sync handlers are removed. The file-reviewed-changed event is kept so DiffFile's own checkbox still flips in lockstep.

Adds browser coverage that a mark flips the sidebar button to its un-mark state through the island, alongside the existing active-highlight-survives-morph guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retiring the Alpine mirror removed the only consumer of ReviewState::reviewedFileMap (the page-root seed). The id-keyed map is fully derivable from reviewedFileIds, so it is dropped from the DTO, the service builder, and toArray, mirroring the earlier visibleFileMap removal. The toArray key guard now also asserts reviewedFileMap stays gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-root mirror

file-reviewed-changed and reviewed-files-reverted are no longer received by a ReviewPage Alpine @window handler (those were removed with the mirror). Both now land only on DiffFile to flip its own checkbox flag; the sidebar and counter refresh through their islands. Updates the resources/CLAUDE.md event schema and the unmarkReviewed comment to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sidebar reviewed button bakes its optimistic toggle direction into a
server-rendered literal at island-render time. The un-mark branch already
emits reviewed-files-reverted, but the mark branch dispatched nothing to the
diff-file. So a rapid sidebar double-click on an already-reviewed file could
leave DiffFile's `reviewed` flag stuck false while server, island, and the
counter all showed reviewed.

Emit file-reviewed-changed from the mark branch, symmetric with the un-mark
revert, so DiffFile converges to the server state regardless of what the
optimistic dispatch carried. No-op on the happy paths.

Cover with a unit test (authoritative mark broadcast) and a browser test
(slot-scoped counter island refreshes on a second normal-mode mark).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The class-header comment claimed five responsibility clusters but the
component has six; Branch Divergence was missing. Update the map.

Fold the duplicated "refresh file list, re-derive session state, inject
orphans" sequence shared by the external-paths and global-gitignore
handlers into reloadSessionAfterFileListChange().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ions

Move two clusters of orchestration out of the review-page SFC into the app
layer, leaving the component as the event/render coordinator.

Divergence: resolveDivergenceState's decision tree becomes the pure
ResolveDivergenceStateAction, returning a DivergenceDecision DTO
(Noop / Aligned / AutoFollow / Show). The comment lookups arrive as closures
so the common aligned path makes no DB query.

Comment writes: addComment / updateComment / deleteComment delegate to
ReviewCommentWorkflowAction, which returns a ReviewCommentMutation the page
applies via applyCommentMutation(). It is an Action, not a service: the arch
tests forbid Livewire from using Services and Services from touching Eloquent
models, and the write path touches the Comment model. Dispatch and skipRender
stay in the SFC so the 1+N hydration contract holds.

Behavior is unchanged. Adds unit tests for both actions and corrects the
pages/CLAUDE.md note that called the extraction a service.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lift the ~250-line inline x-data (the page root and the external-change
poller) into public/js/review-page.js as the reviewPage() and
reviewChangePoller() factories, UMD-shaped like diff-file.js. The remote-link
URL logic becomes the pure, unit-testable computeRemoteMenu(); the @browser
keymap gate is passed in as a config flag; source/visible file entries stay
dataset-backed getters.

Adds Vitest coverage for the remote-link rules, the poller fingerprint
handling, and the reset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…action

The page was a 2,307-line outlier, about 4x the next biggest component. It
stays one Livewire component (the 1+N hydration contract forbids nesting its
sections into children), so the extraction goes into the app layer:

- App\Concerns\ReviewPage\{ReviewsBranchDivergence, ManagesReviewTrash,
  ExportsReview} hold the cohesive clusters with little render coupling. The
  page keeps the lifecycle spine, the comment handlers, the undo() switchboard,
  the reviewed-state render glue, and every computed.
- ReviewCommentWorkflowAction gains clearAll() and restore(), so the last
  direct Comment writes leave the view file. The page applies the result via
  applyCommentMutation(), matching the existing add/update/delete path.
- CountPersistedCommentsAction owns the divergence persisted-comment read,
  keeping the cheap exists() gate alongside the count, because Livewire-layer
  traits delegate model access to Actions.
- The DTO arch allowlist recognizes App\Concerns\ReviewPage as Livewire-layer
  code: DTOs allowed, model access still Action-only.

Behavior-preserving. Pint, PHPStan, Pest, Vitest, and browser suites pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ials

The Settings dropdown and Trash list were the two largest self-contained
markup blocks. They move to resources/views/pages/partials/ behind @include,
which shares the parent scope so wire: bindings and $wire keep resolving. The
islands and toolbar stay inline because they carry the renderIsland targets
the page re-renders selectively.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

Screenshots

Updated for df55e5b.

copy-paths-bulk-default copy-paths-bulk-default
copy-paths-bulk-menu-open copy-paths-bulk-menu-open
copy-paths-single-menu-open copy-paths-single-menu-open

github-actions Bot added a commit that referenced this pull request Jun 16, 2026
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@fgilio, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 25 minutes and 43 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cb261b5c-01bf-4ac3-8dff-0a99c01441d4

📥 Commits

Reviewing files that changed from the base of the PR and between 2f2731c and df55e5b.

📒 Files selected for processing (4)
  • app/Concerns/ReviewPage/ReviewsBranchDivergence.php
  • public/js/review-page.js
  • resources/views/pages/partials/review-trash-list.blade.php
  • tests/Js/review-page.test.js
📝 Walkthrough

Wait — range_98bad44c30d5 appears in two layers (layer_review_page_coordinator and layer_arch_docs). Correcting: removing it from layer_arch_docs and keeping it only in layer_review_page_coordinator per the "exactly once" rule. The arch/docs layer instead uses the CLAUDE.md ranges only.

Walkthrough

Refactors the review page into App\Concerns\ReviewPage traits, centralizes all comment writes through a new ReviewCommentWorkflowAction returning ReviewCommentMutation DTOs, introduces a DivergenceDecision state machine for branch-divergence tracking, converts reviewed-state rendering to Livewire always: true islands, and replaces inline Alpine JS with a new review-page.js UMD module.

Changes

Review Page Refactor

Layer / File(s) Summary
Divergence decision contracts and comment mutation DTO
app/Enums/DivergenceDecisionKind.php, app/DTOs/DivergenceDecision.php, app/DTOs/ReviewCommentMutation.php, app/DTOs/ReviewState.php, app/Services/ReviewStateService.php, tests/Unit/DTOs/ReviewStateTest.php, tests/Unit/Actions/DeriveReviewStateActionTest.php, tests/Unit/Livewire/ReviewPageReviewedUndoTest.php, tests/Unit/Services/ReviewStateServiceTest.php
Adds DivergenceDecisionKind enum (Noop/Aligned/AutoFollow/Show) and DivergenceDecision value object with named static constructors and toArray(). Adds ReviewCommentMutation DTO with factory methods for added/updated/deleted/cleared/restored events carrying checksDivergence and skipsRender flags. Removes reviewedFileMap from ReviewState and ReviewStateService, replacing it with reviewedFileIds.
CountPersistedCommentsAction
app/Actions/CountPersistedCommentsAction.php, tests/Unit/Actions/CountPersistedCommentsActionTest.php
Adds CountPersistedCommentsAction with handle() returning persisted comment count and exists() returning a cheaper boolean, both scoped via Comment::forProjectOrRepo().
ResolveDivergenceStateAction and ReviewsBranchDivergence trait
app/Actions/ResolveDivergenceStateAction.php, app/Concerns/ReviewPage/ReviewsBranchDivergence.php, tests/Unit/Actions/ResolveDivergenceStateActionTest.php
ResolveDivergenceStateAction::handle() walks HEAD state, dismissal timestamps, and lazy comment closures to produce a DivergenceDecision. ReviewsBranchDivergence trait wires this into the Livewire component: polls HEAD, applies four state transitions, exposes switchReviewToHead/keepReviewing/dismiss/undo handlers, and calls UpdateProjectSettingAction for auto-follow.
ReviewCommentWorkflowAction: centralized comment writes
app/Actions/ReviewCommentWorkflowAction.php, tests/Unit/Actions/ReviewCommentWorkflowActionTest.php
Centralizes all comment write operations (add/update/delete/clearAll/restore) into one action that returns ?ReviewCommentMutation. Bulk-clears via Comment::whereIn()->delete() and restores with Comment::updateOrCreate() deduplication.
ExportsReview and ManagesReviewTrash traits
app/Concerns/ReviewPage/ExportsReview.php, app/Concerns/ReviewPage/ManagesReviewTrash.php
ExportsReview adds submitReview/exportSnapshot/copyVisiblePaths/startNewReview. ManagesReviewTrash adds discard/restore/permanentDelete/loadTrashedFiles; the discard flow invalidates DiffCacheKey entries and dispatches undo and fingerprint-reset events.
Review page coordinator: applyCommentMutation, island rendering, reviewed toggles
resources/views/pages/⚡review-page.blade.php, tests/Arch/LayerDependenciesTest.php, resources/views/pages/CLAUDE.md, tests/Unit/Livewire/ReviewPageReviewedUndoTest.php
Routes all comment writes through a new applyCommentMutation() helper. Adds settleReviewedRender() that either triggers island-only re-renders (reviewed-summary + file-list) or a full parent render depending on hide-reviewed mode. Wires file-reviewed-changed/reviewed-files-reverted dispatches. Replaces reloadAfterExternalPathsChange() with reloadSessionAfterFileListChange(). Broadens DTO layer rule to include App\Concerns\ReviewPage.
review-page.js, Blade partials, and status-strip island
public/js/review-page.js, resources/views/pages/partials/review-settings-dropdown.blade.php, resources/views/pages/partials/review-trash-list.blade.php, resources/views/components/status-strip.blade.php, resources/CLAUDE.md, tests/Js/review-page.test.js, tests/Browser/...
Adds review-page.js UMD module: computeRemoteMenu, createReviewPage Alpine component (file selection, scroll-to-comment polling), createChangePoller (fingerprint polling, ⌘R/⌘⇧R). Extracts settings dropdown and trash-list into Blade partials. Wraps reviewed-summary and file-list in always: true islands. Replaces status-strip Alpine counter with injected $reviewedSummary slot.

Sequence Diagram(s)

sequenceDiagram
  participant Alpine as Alpine reviewPage / reviewChangePoller
  participant Livewire as ReviewPage (Livewire)
  participant WorkflowAction as ReviewCommentWorkflowAction
  participant DivergenceAction as ResolveDivergenceStateAction
  participant DB as Comment (Eloquent)

  Alpine->>Livewire: createComment / deleteComment / clearAll / toggleReviewed
  Livewire->>WorkflowAction: handle / delete / clearAll / restore
  WorkflowAction->>DB: persist / delete / updateOrCreate
  WorkflowAction-->>Livewire: ReviewCommentMutation
  Livewire->>Livewire: applyCommentMutation → dispatch comment-updated, undo-available
  Livewire->>DivergenceAction: checkHeadDivergence (if checksDivergence)
  DivergenceAction-->>Livewire: DivergenceDecision
  Livewire->>Livewire: settleReviewedRender → renderIsland OR skipRender
  Livewire-->>Alpine: island re-renders reviewed-summary + file-list
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • fgilio/rfa#47: Adds the head-divergence-poller component that emits head-divergence-transitioned, which the new ReviewsBranchDivergence::checkHeadDivergence() listener directly consumes.
  • fgilio/rfa#82: Updates the head-divergence-poller to emit head-advanced-on-branch, which ReviewsBranchDivergence::refreshAfterHeadAdvance() now handles.
  • fgilio/rfa#96: The ExportsReview::submitReview() logic for surfacing excluded/unplaceable comments depends directly on ExportReviewAction returning excludedComments and submittedIds introduced in that PR.

Poem

🌿 The review page shed its monolith skin,
Traits took the reins, islands crept in.
Divergence decisions now lazily wake,
Comment mutations return what they make.
applyCommentMutation — one ring to rule,
And skipRender skips the full render duel. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main refactoring: decomposing a large review-page component into actions, DTOs, traits, and a JS module while migrating reviewed-state to server-owned Livewire islands.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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 and usage tips.

@fgilio

fgilio commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 2f2731c9cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@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: 4

🧹 Nitpick comments (1)
tests/Js/review-page.test.js (1)

124-202: ⚡ Quick win

Add lifecycle coverage for shortcut registration cleanup.

This suite covers polling/tooltip behavior well, but it doesn’t assert createChangePoller shortcut behavior across init()/destroy() remount cycles. Add one test to verify no duplicate shortcut handlers after re-init.

As per coding guidelines, scripts with non-trivial logic should include focused unit coverage for behavior likely to regress.

🤖 Prompt for 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.

In `@tests/Js/review-page.test.js` around lines 124 - 202, Add a new test case to
the createChangePoller test suite that verifies shortcut handler cleanup across
init/destroy remount cycles. Create a poller instance, call its init() method to
register shortcuts, call destroy() to clean up, then call init() again, and
assert that shortcut handlers are not duplicated after the remount. This ensures
that the destroy() method properly cleans up event listeners so that
re-initializing the poller doesn't accumulate duplicate handlers.

Source: Coding guidelines

🤖 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 `@app/Concerns/ReviewPage/ReviewsBranchDivergence.php`:
- Around line 79-90: The dismissedAtBranch property is missing from the before
and after state snapshots used in change detection within the method that checks
for divergence changes. Add $this->dismissedAtBranch to both the $before and
$after arrays (alongside the existing $this->divergenceState,
$this->divergenceContext, $this->dismissedAtHead, and $this->projectBranch) so
that changes to the dismissed-at-branch state are properly detected and the
caller knows when to re-render the UI.

In `@public/js/review-page.js`:
- Around line 250-266: The init() method registers keymap shortcuts for ⌘R and
⌘⇧R, but the destroy() method does not unregister them, causing handlers to
accumulate on component remounts and trigger multiple refresh actions per
keypress. In the destroy() method, after the existing stopPoll cleanup, add
calls to unregister both keymap shortcuts using the same keymap.register API (or
an unregister method if available) to mirror the registrations done in init()
and prevent handler stacking across component lifecycle cycles.

In `@resources/views/pages/partials/review-trash-list.blade.php`:
- Around line 34-39: The permanentlyDeleteTrashed() method is a destructive
action that lacks integration with the central undo mechanism. Modify the
`@confirmed` event handler on the arm-commit-button component to dispatch the
permanent delete action through the central undo flow by using the
undo-available mechanism and ensuring a corresponding undo() case is added to
handle reversal of the deletion. This ensures that each permanent delete action
creates a single undo entry that can be reversed through the standard undo
interface, consistent with other destructive actions like hide and move.
- Around line 29-39: The restore and delete action controls are currently only
visible on hover, which prevents keyboard users from seeing these buttons when
they navigate via keyboard focus. Update the class attribute on both the restore
button (containing the arrow-uturn-left icon) and the x-arm-commit-button
component to include focus-based visibility classes in addition to the existing
hover classes. Add focus visibility utilities such as group-focus-within or
focus-visible variants to ensure these controls become visible when keyboard
users focus on them, providing parity with mouse users.

---

Nitpick comments:
In `@tests/Js/review-page.test.js`:
- Around line 124-202: Add a new test case to the createChangePoller test suite
that verifies shortcut handler cleanup across init/destroy remount cycles.
Create a poller instance, call its init() method to register shortcuts, call
destroy() to clean up, then call init() again, and assert that shortcut handlers
are not duplicated after the remount. This ensures that the destroy() method
properly cleans up event listeners so that re-initializing the poller doesn't
accumulate duplicate handlers.
🪄 Autofix (Beta)

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: CHILL

Plan: Pro

Run ID: c1612e3b-8fb3-41e9-b1f6-3b39afbfd404

📥 Commits

Reviewing files that changed from the base of the PR and between 166cf24 and 2f2731c.

📒 Files selected for processing (29)
  • app/Actions/CountPersistedCommentsAction.php
  • app/Actions/ResolveDivergenceStateAction.php
  • app/Actions/ReviewCommentWorkflowAction.php
  • app/Concerns/ReviewPage/ExportsReview.php
  • app/Concerns/ReviewPage/ManagesReviewTrash.php
  • app/Concerns/ReviewPage/ReviewsBranchDivergence.php
  • app/DTOs/DivergenceDecision.php
  • app/DTOs/ReviewCommentMutation.php
  • app/DTOs/ReviewState.php
  • app/Enums/DivergenceDecisionKind.php
  • app/Services/ReviewStateService.php
  • public/js/review-page.js
  • resources/CLAUDE.md
  • resources/views/components/status-strip.blade.php
  • resources/views/pages/CLAUDE.md
  • resources/views/pages/partials/review-settings-dropdown.blade.php
  • resources/views/pages/partials/review-trash-list.blade.php
  • resources/views/pages/⚡review-page.blade.php
  • tests/Arch/LayerDependenciesTest.php
  • tests/Browser/FileInteractionTest.php
  • tests/Browser/SessionPersistenceTest.php
  • tests/Js/review-page.test.js
  • tests/Unit/Actions/CountPersistedCommentsActionTest.php
  • tests/Unit/Actions/DeriveReviewStateActionTest.php
  • tests/Unit/Actions/ResolveDivergenceStateActionTest.php
  • tests/Unit/Actions/ReviewCommentWorkflowActionTest.php
  • tests/Unit/DTOs/ReviewStateTest.php
  • tests/Unit/Livewire/ReviewPageReviewedUndoTest.php
  • tests/Unit/Services/ReviewStateServiceTest.php
💤 Files with no reviewable changes (3)
  • tests/Unit/Services/ReviewStateServiceTest.php
  • app/Services/ReviewStateService.php
  • app/DTOs/ReviewState.php

Comment thread app/Concerns/ReviewPage/ReviewsBranchDivergence.php Outdated
Comment thread public/js/review-page.js
Comment thread resources/views/pages/partials/review-trash-list.blade.php
Comment on lines +34 to +39
<x-arm-commit-button
icon="trash"
tooltip="Permanently delete"
@confirmed="$wire.permanentlyDeleteTrashed({{ $trashed['id'] }})"
class="opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add central undo coverage for permanent delete.

permanentlyDeleteTrashed(...) is destructive and currently appears irreversible from this flow. Please wire it into the central undo mechanism so one delete action maps to one undo entry.

Based on learnings, “Every destructive action (hide, delete, move) must offer undo via central mechanism.” As per coding guidelines, destructive actions should use the central undo flow (undo-available + ReviewPage::undo() case).

🤖 Prompt for 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.

In `@resources/views/pages/partials/review-trash-list.blade.php` around lines 34 -
39, The permanentlyDeleteTrashed() method is a destructive action that lacks
integration with the central undo mechanism. Modify the `@confirmed` event handler
on the arm-commit-button component to dispatch the permanent delete action
through the central undo flow by using the undo-available mechanism and ensuring
a corresponding undo() case is added to handle reversal of the deletion. This
ensures that each permanent delete action creates a single undo entry that can
be reversed through the standard undo interface, consistent with other
destructive actions like hide and move.

Sources: Coding guidelines, Learnings

fgilio and others added 3 commits June 16, 2026 13:30
…ction

refreshDivergenceState() compares before/after snapshots to decide whether a
poll tick needs to render. dismissedAtBranch is divergence state (it suppresses
the banner by branch identity) but was absent from both snapshots, so a change
to it alone would go undetected. The dismiss paths also flip divergenceState,
which masked the gap in practice, but the snapshot should be complete on its
own terms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The change-poller registers the refresh shortcuts in init(), but the keymap
store persists across Livewire navigation. Without a matching unregister in
destroy(), a torn-down poller left the refresh combo bound to a dead
component's softRefresh. Unregister both on teardown, and cover the
register/unregister lifecycle in review-page.test.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The restore and permanent-delete controls were hover-only (opacity-0 with
group-hover), so keyboard users could focus controls that stayed visually
hidden. Add group-focus-within:opacity-100 so tabbing into a trash row reveals
its controls, restoring keyboard parity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Jun 16, 2026
@fgilio fgilio merged commit b3b1803 into main Jun 16, 2026
16 checks passed
@fgilio fgilio deleted the refactor/reviewed-state-first-party branch June 16, 2026 11: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.

1 participant