Repository navigation
Implement explicit 2-D and 3-D snow MPM solvers - #175
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📜 Recent review details⏰ Context from checks skipped due to timeout. (16)
📝 WalkthroughWalkthroughSnow MPM support now includes validated constitutive wave-speed calculation, explicit 2-D and 3-D solver lifecycles, adaptive substeps, particle-grid transfers, builders, Python bindings, and unit tests. ChangesSnow MPM simulation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant PythonClient
participant SnowMPMSolver
participant MPMSystemData
participant SnowConstitutiveModel
PythonClient->>SnowMPMSolver: construct and update solver
SnowMPMSolver->>MPMSystemData: transfer particle data to grid
SnowMPMSolver->>SnowConstitutiveModel: compute wave speed and stress
SnowConstitutiveModel-->>SnowMPMSolver: return constitutive values
SnowMPMSolver->>MPMSystemData: update grid and particle state
SnowMPMSolver-->>PythonClient: expose updated solver data
Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 112 |
| Duplication | 2 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
Includes/Core/Solver/Particle/MPM/SnowMPMSolver-Impl.hpp (2)
286-291: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winHoist the per-particle data lookups out of the gradient loop.
ComputeVelocityGradientre-fetchesPositions(),GridVelocities(),DataSize(),GridSpacing(), andDataOrigin()on every call.ComputeMaxVelocityGradientat Line 321 andUpdateDeformationat Line 335 call it once per particle. The lookups therefore repeat for every particle in the system.Pass the array views and grid metadata as parameters, or compute the gradient inside a single loop that fetches them once. This removes redundant work from the per-particle hot path.
Also applies to: 319-322
🤖 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 `@Includes/Core/Solver/Particle/MPM/SnowMPMSolver-Impl.hpp` around lines 286 - 291, Update ComputeVelocityGradient and its callers ComputeMaxVelocityGradient and UpdateDeformation to hoist Positions(), GridVelocities(), DataSize(), GridSpacing(), and DataOrigin() lookups outside the per-particle loop, passing the cached views and metadata into the gradient calculation while preserving existing results.
133-137: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRecord the one-frame lag of
m_maxVelocityGradient.
GetNumberOfSubTimeStepsisconstand readsm_maxVelocityGradient.UpdateDeformationwrites that member during the previous substep, andOnInitializeseeds it. The substep count for the current frame therefore uses the gradient from the previous frame.This lag is intentional for an explicit scheme. Add a short comment that states the invariant.
As per coding guidelines: "keep comments focused on intent, invariants, numerical reasoning, or non-obvious constraints".
🤖 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 `@Includes/Core/Solver/Particle/MPM/SnowMPMSolver-Impl.hpp` around lines 133 - 137, Add a concise comment immediately before the m_maxVelocityGradient timestep limit in GetNumberOfSubTimeSteps, documenting that the value was computed during the previous frame/substep and is intentionally used with a one-frame lag by the explicit scheme. Keep the existing calculation unchanged.Source: Coding guidelines
Includes/Core/Particle/MPM/SnowConstitutiveModel-Impl.hpp (1)
180-188: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider validating before the dependent divisions.
Lines 183-185 validate
totalDeterminantandcurrentDensityafter Lines 153, 180, and 181 already use them. The result stays correct, because IEEE-754 division producesinforNaNand the guard rejects it. However, the check reads as a post-condition on values that were used earlier.Move the
totalDeterminantandcurrentDensitychecks directly after Line 153 to make the pre-conditions explicit, and keep thekappaandwaveSpeedchecks where they are.🤖 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 `@Includes/Core/Particle/MPM/SnowConstitutiveModel-Impl.hpp` around lines 180 - 188, Move the finite-and-positive validation for totalDeterminant and currentDensity to immediately after their computation in the surrounding constitutive-model function, before any dependent divisions or uses. Remove those two checks from the later guard, while keeping the existing kappa and waveSpeed validation there.Tests/UnitTests/SnowMPMSolverTests.cpp (2)
264-275: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument how the expected value 56 is derived.
Line 274 asserts an exact substep count. The value depends on the velocity-gradient limit, the default
m_timeStepLimitScale, and theceilrounding. Any change to those constants breaks the test with no indication of the cause.The derivation is: the lattice rate is 100, the gradient limit gives
0.2 / 100 = 0.002, andceil(0.1 / (0.9 * 0.002))is 56. The value is dimension-independent, which is why the same expectation holds for 2-D and 3-D.Add that derivation as a comment.
♻️ Proposed comment
deforming.Initialize(); + // The velocity-gradient limit binds here: 0.2 / 100 = 0.002 s. With the + // default time-step limit scale of 0.9, ceil(0.1 / (0.9 * 0.002)) = 56. + // The bound is dimension-independent, so 2-D and 3-D agree. EXPECT_EQ(deforming.NumberOfSubTimeSteps(0.1), 56u);🤖 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/UnitTests/SnowMPMSolverTests.cpp` around lines 264 - 275, Add a concise explanatory comment immediately before the EXPECT_EQ assertion in the deforming solver test, documenting that the lattice rate is 100, the velocity-gradient limit yields 0.2 / 100 = 0.002, and ceil(0.1 / (0.9 * 0.002)) produces 56; note that this derivation is dimension-independent.
277-288: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for the constructor parameter validation.
SnowMPMSolver-Impl.hppLines 30-34 reject a non-finite radius, a negative radius, a non-finite mass, and a non-positive mass withstd::invalid_argument.ExpectParametersAndBuildercoversSetTimeStepLimitScalebut not those constructor rejections.Add the matching
EXPECT_THROWassertions so the constructor contract is verified in both dimensions.💚 Proposed additions
EXPECT_THROW( solver.SetTimeStepLimitScale(std::numeric_limits<double>::quiet_NaN()), std::invalid_argument); + + const auto resolution8 = VectorUZ<N>::MakeConstant(8); + const auto unit = VectorD<N>::MakeConstant(1.0); + EXPECT_THROW((SnowMPMSolver<N>{ resolution8, unit, {}, -1.0, 1.0 }), + std::invalid_argument); + EXPECT_THROW((SnowMPMSolver<N>{ resolution8, unit, {}, 0.1, 0.0 }), + std::invalid_argument); + EXPECT_THROW((SnowMPMSolver<N>{ resolution8, unit, {}, 0.1, + std::numeric_limits<double>::infinity() }), + std::invalid_argument);As per coding guidelines: "Update or add tests for both 2-D and 3-D counterparts when behavior applies to both".
🤖 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/UnitTests/SnowMPMSolverTests.cpp` around lines 277 - 288, Add constructor validation coverage to ExpectParametersAndBuilder by adding EXPECT_THROW assertions for SnowMPMSolver construction with non-finite and negative radius values, plus non-finite and non-positive mass values, each expecting std::invalid_argument. Ensure the existing template-based test runs these checks for both 2-D and 3-D solver instantiations.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 `@Includes/Core/Solver/Particle/MPM/SnowMPMSolver-Impl.hpp`:
- Around line 217-224: Adopt one boundary policy for all stencil transfers,
preferably skipping entries whose indices fall outside dataSize rather than
passing them to ClampIndex. Apply it in SnowMPMSolver-Impl.hpp at the
referenceDensity calculation (lines 217-224), impulse transfer (lines 268-277),
and velocity-gradient update (lines 294-309), preserving valid in-domain
entries; add boundary-particle tests covering both supported dimensions.
- Around line 174-178: Update the solver constructor validation near the
existing radius and mass checks to reject any zero component in resolution
before storing or using it. Preserve the existing validation behavior and ensure
ClampIndex only receives positive grid extents.
In `@Includes/Core/Solver/Particle/MPM/SnowMPMSolver.hpp`:
- Around line 14-19: Include SnowConstitutiveModel.hpp directly in
SnowMPMSolver.hpp because SnowConstitutiveModel<N> is stored by value; retain
the existing MPMSystemData.hpp include for CubicBSplineKernel<N>, which does not
require a separate kernel header.
---
Nitpick comments:
In `@Includes/Core/Particle/MPM/SnowConstitutiveModel-Impl.hpp`:
- Around line 180-188: Move the finite-and-positive validation for
totalDeterminant and currentDensity to immediately after their computation in
the surrounding constitutive-model function, before any dependent divisions or
uses. Remove those two checks from the later guard, while keeping the existing
kappa and waveSpeed validation there.
In `@Includes/Core/Solver/Particle/MPM/SnowMPMSolver-Impl.hpp`:
- Around line 286-291: Update ComputeVelocityGradient and its callers
ComputeMaxVelocityGradient and UpdateDeformation to hoist Positions(),
GridVelocities(), DataSize(), GridSpacing(), and DataOrigin() lookups outside
the per-particle loop, passing the cached views and metadata into the gradient
calculation while preserving existing results.
- Around line 133-137: Add a concise comment immediately before the
m_maxVelocityGradient timestep limit in GetNumberOfSubTimeSteps, documenting
that the value was computed during the previous frame/substep and is
intentionally used with a one-frame lag by the explicit scheme. Keep the
existing calculation unchanged.
In `@Tests/UnitTests/SnowMPMSolverTests.cpp`:
- Around line 264-275: Add a concise explanatory comment immediately before the
EXPECT_EQ assertion in the deforming solver test, documenting that the lattice
rate is 100, the velocity-gradient limit yields 0.2 / 100 = 0.002, and ceil(0.1
/ (0.9 * 0.002)) produces 56; note that this derivation is
dimension-independent.
- Around line 277-288: Add constructor validation coverage to
ExpectParametersAndBuilder by adding EXPECT_THROW assertions for SnowMPMSolver
construction with non-finite and negative radius values, plus non-finite and
non-positive mass values, each expecting std::invalid_argument. Ensure the
existing template-based test runs these checks for both 2-D and 3-D solver
instantiations.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 023f1474-dbbc-4130-945a-b418acd3a297
📒 Files selected for processing (10)
Includes/API/Python/Solver/Particle/MPM/SnowMPMSolver.hppIncludes/Core/Particle/MPM/SnowConstitutiveModel-Impl.hppIncludes/Core/Particle/MPM/SnowConstitutiveModel.hppIncludes/Core/Solver/Particle/MPM/SnowMPMSolver-Impl.hppIncludes/Core/Solver/Particle/MPM/SnowMPMSolver.hppSources/API/Python/Solver/Particle/MPM/SnowMPMSolver.cppSources/API/Python/main.cppTests/PythonTests/test_snow_mpm_solver.pyTests/UnitTests/SnowConstitutiveModelTests.cppTests/UnitTests/SnowMPMSolverTests.cpp
📜 Review details
⏰ Context from checks skipped due to timeout. (16)
- GitHub Check: Codacy Static Code Analysis
- GitHub Check: 🧪 Code Coverage - Codecov (Ubuntu 24.04 + gcc-14, ubuntu-24.04, gcc, 14)
- GitHub Check: 🪟 Build - Windows Server 2022 + Visual Studio 2022
- GitHub Check: 🪟 CUDA Build - Windows Server 2022 + Visual Studio 2022 + CUDA 12.6.3 (Release)
- GitHub Check: 🪟 Build - Windows Server 2025 + Visual Studio 2026
- GitHub Check: 🪟 CUDA Build - Windows Server 2025 + Visual Studio 2026 + CUDA 13.2.0 (Release)
- GitHub Check: 🍎 Build - macOS 15.7.4 + Xcode 16.4
- GitHub Check: 🐧 Build - Ubuntu 24.04 + clang-18
- GitHub Check: 🌞 Static Analysis - SonarCloud (Ubuntu 24.04 + gcc-14, ubuntu-24.04, gcc, 14)
- GitHub Check: 🐧 Build - Ubuntu 24.04 + gcc-14
- GitHub Check: 🐧 Build - Ubuntu 24.04 + clang-17
- GitHub Check: 🐧 Build - Ubuntu 24.04 + gcc-13
- GitHub Check: 🐧 Build - Ubuntu 24.04 + clang-16
- GitHub Check: 🐧 Build - Ubuntu 24.04 + gcc-12
- GitHub Check: 🍎 Build - macOS 26.3 + Xcode 26.3
- GitHub Check: 🐧 CUDA Build - Ubuntu 24.04 + gcc-12 + CUDA 12.6.3
🧰 Additional context used
📓 Path-based instructions (9)
Sources/API/Python/**/*.{cpp,hpp}
📄 CodeRabbit inference engine (AGENTS.md)
Keep Python binding implementations synchronized with core APIs, use existing Python names and camelCase property conventions, and do not mechanically expose C++ spelling.
Files:
Sources/API/Python/main.cppSources/API/Python/Solver/Particle/MPM/SnowMPMSolver.cpp
Sources/API/Python/main.cpp
📄 CodeRabbit inference engine (AGENTS.md)
Register every exposed binding in
main.cppand preserve dependency order.
Files:
Sources/API/Python/main.cpp
**/*.{cpp,cc,cxx,h,hpp,cu,cuh}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{cpp,cc,cxx,h,hpp,cu,cuh}: Treat warnings as errors under the defaultCUBBYFLOW_WARNINGS_AS_ERRORS=ON; fix project warnings instead of globally suppressing them.
Follow.clang-format: four-space indentation, 80-column C++ limit, sorted includes, project brace style, and format only touched C++/CUDA files.
Preserve existing copyright headers and keep comments focused on intent, invariants, numerical reasoning, or non-obvious constraints.
Files:
Sources/API/Python/main.cppIncludes/API/Python/Solver/Particle/MPM/SnowMPMSolver.hppIncludes/Core/Particle/MPM/SnowConstitutiveModel.hppSources/API/Python/Solver/Particle/MPM/SnowMPMSolver.cppIncludes/Core/Particle/MPM/SnowConstitutiveModel-Impl.hppTests/UnitTests/SnowMPMSolverTests.cppIncludes/Core/Solver/Particle/MPM/SnowMPMSolver-Impl.hppTests/UnitTests/SnowConstitutiveModelTests.cppIncludes/Core/Solver/Particle/MPM/SnowMPMSolver.hpp
**/*
📄 CodeRabbit inference engine (AGENTS.md)
**/*: Keep 2-D and 3-D behavior aligned; inspect sibling implementations, aliases, explicit instantiations, bindings, and tests before changing one dimensional side.
Keep the public C++ API and Python-visible behavior synchronized when changing public types, methods, defaults, enums, or solver behavior.
Fix shared behavior at the shared layer; search callers, overrides, bindings, tests, and dimensional specializations before editing, and avoid one-off caller guards when an invariant belongs in a common utility or base class.
Use existing patterns and dependencies before adding abstractions, libraries, templates, builders, numerical helpers, parallel loops, serialization code, or tests.
Run the smallest relevant validation: focused C++ tests for core behavior, both dimensions for shared templates, focused pytest for Python APIs, CUDA compilation/tests for CUDA changes, fresh configure/build for build changes, and round-trip tests for serialization.
Do not commit build output, test logs, caches, IDE state, generated build files, or unrelated formatting; keep commits focused and use the appropriate conventional prefix.
Before handoff, confirm the fix is at the shared behavior source, dimensional/Python/CUDA surfaces are synchronized when relevant, focused checks pass, touched C++/CUDA files are formatted, and skipped platform/GPU/performance validation is explicitly reported.
Files:
Sources/API/Python/main.cppIncludes/API/Python/Solver/Particle/MPM/SnowMPMSolver.hppTests/PythonTests/test_snow_mpm_solver.pyIncludes/Core/Particle/MPM/SnowConstitutiveModel.hppSources/API/Python/Solver/Particle/MPM/SnowMPMSolver.cppIncludes/Core/Particle/MPM/SnowConstitutiveModel-Impl.hppTests/UnitTests/SnowMPMSolverTests.cppIncludes/Core/Solver/Particle/MPM/SnowMPMSolver-Impl.hppTests/UnitTests/SnowConstitutiveModelTests.cppIncludes/Core/Solver/Particle/MPM/SnowMPMSolver.hpp
Includes/API/Python/**/*.{hpp,h}
📄 CodeRabbit inference engine (AGENTS.md)
Keep Python binding declarations synchronized with public Python-visible C++ APIs.
Files:
Includes/API/Python/Solver/Particle/MPM/SnowMPMSolver.hpp
Tests/PythonTests/test_*.py
📄 CodeRabbit inference engine (AGENTS.md)
Tests/PythonTests/test_*.py: Add or preserve focused pytest coverage for every new or changed Python-visible API.
Name Python teststest_type.pyand run focused pytest files for Python API changes.
Files:
Tests/PythonTests/test_snow_mpm_solver.py
Includes/Core/**/*.hpp
📄 CodeRabbit inference engine (AGENTS.md)
Keep public C++ declarations and Doxygen comments under
Includes/Core/; use project includes such as<Core/...>and theCubbyFlownamespace.
Files:
Includes/Core/Particle/MPM/SnowConstitutiveModel.hppIncludes/Core/Particle/MPM/SnowConstitutiveModel-Impl.hppIncludes/Core/Solver/Particle/MPM/SnowMPMSolver-Impl.hppIncludes/Core/Solver/Particle/MPM/SnowMPMSolver.hpp
Includes/Core/**/*.{hpp,h}
📄 CodeRabbit inference engine (AGENTS.md)
For dimensional templates, keep dimension-independent logic shared, preserve
Foo2/Foo3and pointer aliases, and follow existing builder and ownership APIs.
Files:
Includes/Core/Particle/MPM/SnowConstitutiveModel.hppIncludes/Core/Particle/MPM/SnowConstitutiveModel-Impl.hppIncludes/Core/Solver/Particle/MPM/SnowMPMSolver-Impl.hppIncludes/Core/Solver/Particle/MPM/SnowMPMSolver.hpp
Tests/UnitTests/**/*.{cpp,hpp}
📄 CodeRabbit inference engine (AGENTS.md)
Tests/UnitTests/**/*.{cpp,hpp}: Update or add tests for both 2-D and 3-D counterparts when behavior applies to both; use GoogleTest/GMock macros and focused regression scenarios.
Use the existingRESOURCES_DIRcompile definition for C++ fixtures rather than relying on the current working directory.
Files:
Tests/UnitTests/SnowMPMSolverTests.cppTests/UnitTests/SnowConstitutiveModelTests.cpp
🔇 Additional comments (21)
Includes/API/Python/Solver/Particle/MPM/SnowMPMSolver.hpp (1)
1-19: LGTM!Sources/API/Python/Solver/Particle/MPM/SnowMPMSolver.cpp (1)
11-47: LGTM!Sources/API/Python/main.cpp (1)
95-95: LGTM!Also applies to: 335-336
Tests/PythonTests/test_snow_mpm_solver.py (1)
1-39: LGTM!Includes/Core/Particle/MPM/SnowConstitutiveModel.hpp (1)
93-107: LGTM!Includes/Core/Particle/MPM/SnowConstitutiveModel-Impl.hpp (3)
116-116: LGTM!
193-205: LGTM!
159-181: 🩺 Stability & AvailabilityNo change required:
SVDnormalizes every singular value to a non-negative value. The fixed-size implementation explicitly negates negative values before returning them.Tests/UnitTests/SnowConstitutiveModelTests.cpp (1)
164-218: LGTM!Also applies to: 381-399
Includes/Core/Solver/Particle/MPM/SnowMPMSolver.hpp (2)
29-96: LGTM!Also applies to: 125-128
103-111: 📐 Maintainability & Code QualityKeep
[[nodiscard]]on these setters. Existing CubbyFlow builders use the same annotation on chainable setters, including both 2-D and 3-D builders.> Likely an incorrect or invalid review comment.Includes/Core/Solver/Particle/MPM/SnowMPMSolver-Impl.hpp (8)
21-40: LGTM!
42-71: LGTM!
152-156: LGTM!
183-216: LGTM!Also applies to: 226-233
249-266: LGTM!Also applies to: 269-280
327-342: LGTM!Also applies to: 344-396
146-149: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winFix the upper clamp bound to avoid an out-of-range conversion.
std::numeric_limits<unsigned int>::max()is 4294967295. A 32-bitunsigned intvalue cannot be represented exactly as adoublein this case, so the conversion at Line 147 rounds up to 4294967296.0. Whencountreaches or exceeds that value,std::clampreturns 4294967296.0 and the cast at Line 149 converts a value that the target type cannot represent. That conversion is undefined behavior.This path is reachable. A very small
desiredTimeStep, which follows from a large wave speed or a large velocity gradient, drivescountabove the bound.Clamp with a value that is exactly representable and strictly below the maximum.
🐛 Proposed fix
const double count = std::ceil(timeIntervalInSeconds / (m_timeStepLimitScale * desiredTimeStep)); - const double maxCount = - static_cast<double>(std::numeric_limits<unsigned int>::max()); - - return static_cast<unsigned int>(std::clamp(count, 1.0, maxCount)); + // Largest double that is exactly representable and converts safely to + // unsigned int. + constexpr double maxCount = 4294967295.0 - 1023.0; + + return static_cast<unsigned int>(std::clamp(count, 1.0, maxCount)); }A clearer alternative avoids the magic constant:
constexpr unsigned int maxSteps = std::numeric_limits<unsigned int>::max(); if (!(count < static_cast<double>(maxSteps))) { return maxSteps; } return static_cast<unsigned int>(std::max(count, 1.0));> Likely an incorrect or invalid review comment.
158-166: 🩺 Stability & AvailabilityKeep the custom
OnBeginAdvanceTimeStepsequence. TheParticleSystemSolver2andParticleSystemSolver3base implementations are empty, so omitting a base call does not skip per-step setup.> Likely an incorrect or invalid review comment.Tests/UnitTests/SnowMPMSolverTests.cpp (2)
14-97: LGTM!Also applies to: 99-221, 290-309, 325-358
312-323: 📐 Maintainability & Code QualityNo shared
RUN_FOR_2D_AND_3Dmacro exists.The two translation units contain separate duplicate definitions. No shared-header change is required.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #175 +/- ##
==========================================
+ Coverage 83.43% 83.57% +0.14%
==========================================
Files 416 418 +2
Lines 23568 23807 +239
==========================================
+ Hits 19663 19897 +234
- Misses 3905 3910 +5 🚀 New features to boost your workflow:
|
|



This revision includes:
Summary by CodeRabbit
New Features
Tests