[Cursor Review] feat(world/view_layer.go): implement block view layer#29
[Cursor Review] feat(world/view_layer.go): implement block view layer#29HashimTheArab wants to merge 50 commits into
Conversation
|
@cursor review |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds per-position view-layer block overrides and integrates them into session APIs, chunk serialization, and player block-breaking; introduces cloneable chunk components and updates handler signatures to indicate private (view) vs public (world) breaks. ChangesView-Layer Block Override Feature
Sequence DiagramsequenceDiagram
participant Player
participant Session
participant Handler
participant World
participant Particles
Player->>Session: check privateBlock(pos)
Session-->>Player: effective block (private? true/false)
Player->>Handler: HandleStartBreak(ctx,pos,private)
Player->>World: StartCrackAction(using effective block)
Player->>World: Finish breaking at pos
World->>Handler: HandleBlockBreak(ctx,pos,private,&drops,&xp)
alt private == true
World->>Particles: spawn private break particles (player-only)
World->>Session: ViewPublicBlock(pos)
else
World->>Particles: spawn public/world break particles
World->>World: apply world block removal and drops
end
🎯 3 (Moderate) | ⏱️ ~25 minutes
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/player/player.go (1)
1917-1928:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard against stale
breakingPosin the!p.breakingpath.
p.breakingPosis only authoritative while a break is actually in progress (or just after a creative-modeStartBreakingearly-returns). WhenFinishBreakingis invoked with!p.breakingand there was no priorStartBreakingthis tick,p.breakingPosis either the zero valuecube.Pos{}or whatever was last set. If a private block happens to exist at that stale position, this new branch will callBreakBlockagainst it.Consider only entering the private fast-path when you can confirm the position is current, e.g. by also requiring that
StartBreakingset it during this interaction (or by passing the pos intoFinishBreaking).🤖 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 `@server/player/player.go` around lines 1917 - 1928, FinishBreaking currently uses p.breakingPos even when p.breaking is false, which can be stale; update the logic so the private fast-path only runs for a current, authoritative position—either change FinishBreaking to accept the target pos (preferred) or add/consult a validity flag set by StartBreaking (e.g. p.breakingPosValid true in StartBreaking and cleared in AbortBreaking/after FinishBreaking) and require p.breakingPosValid && p.breakingPos != zero before calling privateBlock(...) and BreakBlock; reference Player.FinishBreaking, p.breaking, p.breakingPos, StartBreaking, AbortBreaking, privateBlock and BreakBlock when making the change.
🧹 Nitpick comments (2)
server/session/view_layer.go (1)
86-95: 💤 Low valueInconsistent nil-guard on
s.viewLayer.Every other helper in this file early-returns when
s.viewLayer == nil, butViewLayerBlockChangeddirectly dereferencess.viewLayer.Block(pos)at Line 88. In current usage this is the layer's own callback so it should be safe, but a future caller (or a session that never had a view layer wired up) would NPE. Consider matching the pattern used byViewBlock/ViewPublicBlockfor defensive consistency.🛡️ Proposed nil-guard
func (s *Session) ViewLayerBlockChanged(pos cube.Pos) { + if s.viewLayer == nil { + return + } if b, ok := s.viewLayer.Block(pos); ok { s.viewBlockUpdate(pos, b, 0) return } if b, ok := s.publicBlock(pos); ok { s.viewBlockUpdate(pos, b, 0) } }🤖 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 `@server/session/view_layer.go` around lines 86 - 95, ViewLayerBlockChanged currently dereferences s.viewLayer without a nil-guard which can cause an NPE; add the same defensive early-return used by ViewBlock/ViewPublicBlock by checking if s.viewLayer == nil at the start of ViewLayerBlockChanged and return immediately if so, then proceed to call s.viewLayer.Block(pos) and fall back to s.publicBlock(pos) as before, ensuring you still call s.viewBlockUpdate(pos, b, 0) for whichever block is found.server/player/player.go (1)
1841-1842: 💤 Low valueRedundant view-layer lookup.
breakingBlock(pos)already callsprivateBlock(pos)internally, then Line 1842 callsprivateBlock(pos)again just to read the boolean. Consider returning both values from a single helper to avoid the double lookup (and any chance the two reads disagree if the override is mutated concurrently).♻️ Proposed helper
- b := p.breakingBlock(pos) - _, private := p.privateBlock(pos) + b, private := p.breakingBlock(pos)…and update
breakingBlock:-func (p *Player) breakingBlock(pos cube.Pos) world.Block { - if b, ok := p.privateBlock(pos); ok { - return b - } - return p.tx.Block(pos) -} +func (p *Player) breakingBlock(pos cube.Pos) (world.Block, bool) { + if b, ok := p.privateBlock(pos); ok { + return b, true + } + return p.tx.Block(pos), false +}Then adjust the
ContinueBreakingcall site to discard the boolean.
🤖 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 `@server/player/player.go`:
- Around line 2054-2058: The private-branch call to p.Handler().HandleBlockBreak
passes drops and xp pointers but never applies them; after the handler returns
(and if ctx.Cancelled() is false) iterate the drops slice returned via drops and
add each item.Stack to the player (e.g. via p.Inventory() add/transfer logic or
the same spawn-vs-inventory behavior used in the world-block branch) and call
p.AddExperience(xp) to grant the earned XP; alternatively, if you prefer not to
mutate API, remove the drops and xp pointer parameters from HandleBlockBreak and
its callers (update the handler signature) so the private branch cannot
misleadingly accept them.
- Around line 2052-2066: The private-break branch leaks visuals to other players
and incorrectly restores the public block on cancel: make the break particle
local to the player by replacing the broadcast
p.tx.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: privateBlock})
with a player-local particle send (use whatever per-player particle API or
method on Player/interface — e.g., a p.SendParticle or scoped send) so only this
client sees the privateBlock particle; on cancel, stop calling
p.resendNearbyBlocks(pos) (which re-sends p.tx.Block(pos)) and instead re-assert
the player's private override using the layer-specific update routine (e.g.,
call ViewLayerBlockChanged or the equivalent per-player block override resend
for pos with privateBlock) so the client keeps its private view; remove the dead
variable b := p.tx.Block(pos) from this branch (or confirm and document if its
omission of Exhaust(0.005) and tool durability handling is intentional), and if
those survival side-effects were expected include p.Exhaust and durability logic
here as in the public-break path.
---
Outside diff comments:
In `@server/player/player.go`:
- Around line 1917-1928: FinishBreaking currently uses p.breakingPos even when
p.breaking is false, which can be stale; update the logic so the private
fast-path only runs for a current, authoritative position—either change
FinishBreaking to accept the target pos (preferred) or add/consult a validity
flag set by StartBreaking (e.g. p.breakingPosValid true in StartBreaking and
cleared in AbortBreaking/after FinishBreaking) and require p.breakingPosValid &&
p.breakingPos != zero before calling privateBlock(...) and BreakBlock; reference
Player.FinishBreaking, p.breaking, p.breakingPos, StartBreaking, AbortBreaking,
privateBlock and BreakBlock when making the change.
---
Nitpick comments:
In `@server/session/view_layer.go`:
- Around line 86-95: ViewLayerBlockChanged currently dereferences s.viewLayer
without a nil-guard which can cause an NPE; add the same defensive early-return
used by ViewBlock/ViewPublicBlock by checking if s.viewLayer == nil at the start
of ViewLayerBlockChanged and return immediately if so, then proceed to call
s.viewLayer.Block(pos) and fall back to s.publicBlock(pos) as before, ensuring
you still call s.viewBlockUpdate(pos, b, 0) for whichever block is found.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 18a6965e-ffc1-48f2-acb8-f09799d84d06
📒 Files selected for processing (10)
server/player/handler.goserver/player/player.goserver/session/chunk.goserver/session/view_layer.goserver/session/world.goserver/world/chunk/chunk.goserver/world/chunk/palette.goserver/world/chunk/paletted_storage.goserver/world/chunk/sub_chunk.goserver/world/view_layer.go
|
@cursor review |
|
@cursor review |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
server/player/player.go (2)
1883-1886:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep private break feedback session-local.
bcan now come from a per-player override, but these crack actions, particles, and block-breaking sounds still go throughp.viewers()/p.tx.*. Other nearby players will see break progress for a block they never had at that position. The private path should send feedback only top.session()/p.ShowParticle/p.PlaySound, while shared broadcasts should keep using the public world block.Also applies to: 1947-1961, 2097-2099
🤖 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 `@server/player/player.go` around lines 1883 - 1886, The crack-action and other break-feedback currently use p.viewers() and public broadcasts but must respect per-player overrides: replace sending private break feedback (the result of p.breakTime(b) stored in p.lastBreakDuration and the subsequent viewer.ViewBlockAction calls) with session-local methods so only the acting player receives it—use p.session(), p.ShowParticle and p.PlaySound (or other p.session-scoped senders) for the private path; keep the shared/world broadcasts (the public world block updates and p.tx.* broadcasts) unchanged. Update the occurrences around p.lastBreakDuration/p.breakTime(b) and the similar blocks at the other noted places (lines ~1947-1961 and ~2097-2099) so private feedback is sent via session-local APIs and only shared broadcasts remain on p.viewers()/p.tx.*.
2050-2064:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't resend the public block after a private-break failure.
When
privateis true,resendNearbyBlocks(pos)re-sendsp.tx.Block(pos), which overwrites the client's override with the public block on cancel / range failure / unbreakable failure. That leaves the player desynced until some later view-layer refresh happens. Re-assert the view-layer state instead of using the world resend path for private breaks.Suggested shape
+ resendBrokenPos := func() { + if private { + p.session().ViewLayerBlockChanged(pos) + return + } + p.resendNearbyBlocks(pos) + } + if !p.canReach(pos.Vec3Centre()) || !p.GameMode().AllowsEditing() { - p.resendNearbyBlocks(pos) + resendBrokenPos() return } if _, breakable := b.(block.Breakable); !breakable && !p.GameMode().CreativeInventory() { - p.resendNearbyBlocks(pos) + resendBrokenPos() return } ... if p.Handler().HandleBlockBreak(ctx, pos, private, &drops, &xp); ctx.Cancelled() { - p.resendNearbyBlocks(pos) + resendBrokenPos() return }Also applies to: 2077-2079
🤖 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 `@server/player/player.go` around lines 2050 - 2064, When handling a private break (privateBlock, private := p.privateBlock(pos)), do not call p.resendNearbyBlocks(pos) because that uses p.tx.Block(pos) and will overwrite the client's private override with the public world block; instead re-assert the view-layer private state by sending the privateBlock to the client using the same channel used for overrides (e.g. the method that previously sends privateBlock/overrides to the client rather than p.tx.Block), and apply the same change for the other early-return paths (range/cancel/unbreakable failures around lines 2077-2079) so failures send the private override back to the player rather than resending the public world block.
🤖 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 `@server/player/player.go`:
- Around line 2084-2095: The BreakHandler is being invoked even for private
breaks, which lets handlers inspect/mutate the public world; change the logic so
handlers run only when the break is not private. Concretely, guard the
BreakHandler call with the same private check (e.g., move the
info.BreakHandler(pos, p.tx, p) invocation into the else branch where
p.tx.SetBlock is used, or add if !private before calling it) so that when
private is true (the code path that calls p.ViewPublicBlock) the
BreakInfo.BreakHandler is skipped.
---
Duplicate comments:
In `@server/player/player.go`:
- Around line 1883-1886: The crack-action and other break-feedback currently use
p.viewers() and public broadcasts but must respect per-player overrides: replace
sending private break feedback (the result of p.breakTime(b) stored in
p.lastBreakDuration and the subsequent viewer.ViewBlockAction calls) with
session-local methods so only the acting player receives it—use p.session(),
p.ShowParticle and p.PlaySound (or other p.session-scoped senders) for the
private path; keep the shared/world broadcasts (the public world block updates
and p.tx.* broadcasts) unchanged. Update the occurrences around
p.lastBreakDuration/p.breakTime(b) and the similar blocks at the other noted
places (lines ~1947-1961 and ~2097-2099) so private feedback is sent via
session-local APIs and only shared broadcasts remain on p.viewers()/p.tx.*.
- Around line 2050-2064: When handling a private break (privateBlock, private :=
p.privateBlock(pos)), do not call p.resendNearbyBlocks(pos) because that uses
p.tx.Block(pos) and will overwrite the client's private override with the public
world block; instead re-assert the view-layer private state by sending the
privateBlock to the client using the same channel used for overrides (e.g. the
method that previously sends privateBlock/overrides to the client rather than
p.tx.Block), and apply the same change for the other early-return paths
(range/cancel/unbreakable failures around lines 2077-2079) so failures send the
private override back to the player rather than resending the public world
block.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
|
@cursor review |
|
@cursor reivew, maybe? |
|
Unable to authenticate your request. Please make sure to connect your GitHub account to Cursor. Go to Cursor |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
server/player/player.go (1)
2076-2080:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCancel path still overwrites the private override with the world block.
When
HandleBlockBreakcancels a private break,p.resendNearbyBlocks(pos)ultimately callsSession.ViewBlockUpdate(pos, p.tx.Block(pos), 0), which is the public block. For a private view-layer block the client was seeing, this resends the underlying world block and wipes the override — the opposite of the intended "cancel" behavior. The override should be re-asserted instead (e.g. viaViewLayerBlockChanged).🛠️ Suggested fix
- if p.Handler().HandleBlockBreak(ctx, pos, private, &drops, &xp); ctx.Cancelled() { - p.resendNearbyBlocks(pos) - return - } + if p.Handler().HandleBlockBreak(ctx, pos, private, &drops, &xp); ctx.Cancelled() { + if private { + p.session().ViewLayerBlockChanged(pos) + } else { + p.resendNearbyBlocks(pos) + } + return + }🤖 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 `@server/player/player.go` around lines 2076 - 2080, The cancel path after calling p.Handler().HandleBlockBreak currently calls p.resendNearbyBlocks(pos) which ends up invoking Session.ViewBlockUpdate(pos, p.tx.Block(pos), 0) and thus overwrites any private view-layer override; instead, detect when the cancelled break was on a private/view-layer block and re-assert that override rather than resending the world block — e.g. replace the p.resendNearbyBlocks(pos) call in the ctx.Cancelled() branch with logic that uses the session's view-layer update method (use ViewLayerBlockChanged or the equivalent Session method) to send the private view-layer block back to the client (using the same override value you passed into HandleBlockBreak) so the override is preserved.
🧹 Nitpick comments (1)
server/player/player.go (1)
2084-2086: 💤 Low valueUse the safe session accessor / existing helper for the per-player particle.
p.s.ViewParticle(...)bypasses thesession.Nopguard used everywhere else in this file. A nilp.s(or a Nop session ever reaching this path) would panic, and the direct field access is inconsistent with the rest ofBreakBlock(which goes throughp.session()).ShowParticlealready does exactly this dispatch.♻️ Suggested refactor
if private { p.ViewPublicBlock(pos) - p.s.ViewParticle(pos.Vec3Centre(), particle.BlockBreak{Block: b}) + p.ShowParticle(pos.Vec3Centre(), particle.BlockBreak{Block: b}) } else {🤖 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 `@server/player/player.go` around lines 2084 - 2086, Replace the direct session field access p.s.ViewParticle(...) with the safe accessor used elsewhere: call the existing helper that dispatches to either the real session or session.Nop (e.g., use p.session() or the ShowParticle wrapper) so nil or Nop sessions won't panic; specifically, inside BreakBlock replace p.s.ViewParticle(pos.Vec3Centre(), particle.BlockBreak{Block: b}) with the helper call (ShowParticle or p.session().ViewParticle via p.session()) that mirrors other uses in BreakBlock.
🤖 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 `@server/player/player.go`:
- Line 2090: The if-statement `if breakable, ok := b.(block.Breakable); ok {`
has extra spacing before the opening brace; update the `player.go` occurrence so
there is a single space before `{` (i.e., change the condition to `if breakable,
ok := b.(block.Breakable); ok {`) to satisfy gofmt/golangci-lint while keeping
the same variables `breakable`, `ok`, and the type assertion to
`block.Breakable`.
---
Duplicate comments:
In `@server/player/player.go`:
- Around line 2076-2080: The cancel path after calling
p.Handler().HandleBlockBreak currently calls p.resendNearbyBlocks(pos) which
ends up invoking Session.ViewBlockUpdate(pos, p.tx.Block(pos), 0) and thus
overwrites any private view-layer override; instead, detect when the cancelled
break was on a private/view-layer block and re-assert that override rather than
resending the world block — e.g. replace the p.resendNearbyBlocks(pos) call in
the ctx.Cancelled() branch with logic that uses the session's view-layer update
method (use ViewLayerBlockChanged or the equivalent Session method) to send the
private view-layer block back to the client (using the same override value you
passed into HandleBlockBreak) so the override is preserved.
---
Nitpick comments:
In `@server/player/player.go`:
- Around line 2084-2086: Replace the direct session field access
p.s.ViewParticle(...) with the safe accessor used elsewhere: call the existing
helper that dispatches to either the real session or session.Nop (e.g., use
p.session() or the ShowParticle wrapper) so nil or Nop sessions won't panic;
specifically, inside BreakBlock replace p.s.ViewParticle(pos.Vec3Centre(),
particle.BlockBreak{Block: b}) with the helper call (ShowParticle or
p.session().ViewParticle via p.session()) that mirrors other uses in BreakBlock.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
|
@cursor review |
|
@cursor review |
|
@cursor review |
Restore the existing session constructor pattern for selecting the block registry after initialising the session. NewViewLayer no longer needs the registry, so the temporary local was unnecessary.\n\nConstraint: Review feedback identified the precomputed block registry local as unnecessary.\nRejected: Keeping br in the struct literal | It creates avoidable churn from the existing constructor flow.\nConfidence: high\nScope-risk: narrow\nDirective: Keep registry fallback assignment near other post-construction session setup unless a constructor dependency requires it earlier.\nTested: make lint; go test ./server/session; go test ./server/...\nNot-tested: Custom block registry integration smoke test.
When a private block override disappears while the player is still finishing a private break, resend the public block instead of silently returning. This corrects client prediction without breaking the public block.\n\nConstraint: Private break completion must not mutate the public block if the private override vanished mid-break.\nRejected: Falling through to BreakBlock | It can break the public block for a stale private interaction.\nConfidence: high\nScope-risk: narrow\nDirective: Stale private interactions should resynchronise the client rather than mutate public world state.\nTested: make lint; go test ./server/player ./server/session; go test ./server/...\nNot-tested: Manual client finish packet after plugin removes private override mid-break.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 223aae4. Configure here.
Merge upstream master into the view-layer branch and keep upstream chunk clone/cache-light handling while preserving the branch's view-layer changes.\n\nConstraint: GitHub reported conflicts in chunk clone/storage files after upstream chunk optimisations.\nRejected: Keeping older append-based clones | Upstream slices.Clone and light sentinel preservation are the newer base behavior.\nConfidence: high\nScope-risk: moderate\nDirective: Preserve upstream chunk storage optimisations when resolving future conflicts in this area.\nTested: make lint; go test ./server/player ./server/session ./server/world/chunk ./server/block/cube/trace; go test ./server/...\nNot-tested: Manual chunk light/cache regression scenarios.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 04ba75cbf5
ℹ️ 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".
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…player has no suitable item to insert projectile into it (df-mc#1272) * crossbow.go: Fix crossbow charging animation played even if the player has no suitable item to insert suitable projectile into it * crossbow.go: use blank identifier for unused "tx" variable
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72a16a83ba
ℹ️ 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".
Cursor review mirror of df-mc#1249
Original: df-mc#1249
Note
Medium Risk
Touches core player block interaction, chunk encoding, and client sync paths; incorrect override handling could desync clients or leak private edits to the world.
Overview
Adds per-player block view layers alongside the existing entity view-layer: a player can see fake blocks at positions without changing the real world, and revert with
ViewPublicBlock.ViewLayer now stores block overrides (
ViewBlock,Block,Blocks) and notifies sessions when they change. Chunk / SubChunk / Palette gainClone()so chunk data can be copied before applying overrides. Session merges overrides into chunks sent to the client (applyViewLayerToChunk), applies them onViewBlockUpdate, and refreshes overrides viaViewLayerBlockChanged(including sub-chunk height adverts when needed).Player interaction uses the visible block (
viewedBlock/breakingBlock): use/place on private blocks is blocked; breaking private blocks clears the override, shows break effects only to that player (no world drops/XP), and crack/particles/sounds route throughViewPrivateBlockActionwhile skipping viewers who have their own override at that position.Reviewed by Cursor Bugbot for commit 223aae4. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Refactor