fix: Global variables support - types and debugger mapping#520
Conversation
Bring the same types from program POU variable table to global variables: - Add function block types (system and user libraries) - Add array type support with GlobalArrayModal - Add search/filter functionality for all type categories This matches the functionality available in the regular POU variable table. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When forcing or viewing an external variable in a POU, the debugger now correctly maps it to the corresponding global variable (CONFIG0__ prefix) instead of looking for a local POU variable (RES0__ prefix). This ensures that: - Forcing an external variable affects the actual global variable - All POUs with the same external variable see the same value - Global variables can be properly debugged Changes: - Updated matchVariableWithDebugEntry to handle external variables - Added matchGlobalVariableWithDebugEntry for direct global var matching - Updated buildDebugTree and related functions to use correct paths for external variables (CONFIG0__ vs RES0__) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
WalkthroughAdds a GlobalArrayModal component and integrates it into the type-selection UI; introduces categorized, searchable type and library submenus; centralizes variable base-path computation and extends debug-variable matching to handle external/global variables. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant SelectableTypeCell
participant GlobalArrayModal
participant DimensionsModal
participant OpenPLC_Store
User->>SelectableTypeCell: Click "Array"
SelectableTypeCell->>GlobalArrayModal: open(arrayModalIsOpen, variableName)
GlobalArrayModal->>OpenPLC_Store: read globalVariables, dataTypes, libraries
OpenPLC_Store-->>GlobalArrayModal: return variable & type data
GlobalArrayModal->>DimensionsModal: render with categorized types & handlers
User->>DimensionsModal: edit dimensions / select base type
DimensionsModal-->>GlobalArrayModal: emit dimension/type updates
User->>GlobalArrayModal: Save
GlobalArrayModal->>GlobalArrayModal: validate & format array type
GlobalArrayModal->>OpenPLC_Store: updateVariable(new array-type payload)
OpenPLC_Store-->>GlobalArrayModal: confirm
GlobalArrayModal->>SelectableTypeCell: close modal / propagate close
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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: 1
🤖 Fix all issues with AI agents
In
@src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx:
- Around line 93-106: handleRearrangeDimensions is mutating the dimensions array
with splice; change it to perform immutable updates instead: create a copy of
dimensions (e.g., [...dimensions]), remove and insert the item in the copy using
non-mutating operations (slice/spread), then call setDimensions(newDimensions)
and setSelectedInput with the new index string; keep the same boundary checks
and use the same function name handleRearrangeDimensions and state setters
(setDimensions, setSelectedInput).
🧹 Nitpick comments (1)
src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx (1)
142-145: Consider simplifying base type check.The
forEachloop can be replaced with a more conciseincludes()check for improved readability.♻️ Suggested simplification
- let isBaseType = false - baseTypes.forEach((type) => { - if (type === typeValue) isBaseType = true - }) + const isBaseType = baseTypes.includes(typeValue)
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsxsrc/renderer/components/_molecules/global-variables-table/selectable-cell.tsxsrc/renderer/components/_organisms/workspace-activity-bar/default.tsxsrc/renderer/utils/debug-tree-builder.tssrc/renderer/utils/parse-debug-file.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-16T14:31:51.786Z
Learnt from: vmleroy
Repo: Autonomy-Logic/openplc-editor PR: 265
File: src/renderer/components/_atoms/graphical-editor/ladder/block.tsx:149-153
Timestamp: 2025-06-16T14:31:51.786Z
Learning: In src/renderer/components/_atoms/graphical-editor/ladder/block.tsx, the libraries.system data is dynamic and imported from JSON with varying structure, making proper static typing infeasible. The ts-expect-error and eslint-disable comments are justified when dynamically probing the structure at runtime.
Applied to files:
src/renderer/components/_molecules/global-variables-table/selectable-cell.tsx
📚 Learning: 2025-03-14T14:46:03.671Z
Learnt from: JoaoGSP
Repo: Autonomy-Logic/openplc-editor PR: 218
File: src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx:73-73
Timestamp: 2025-03-14T14:46:03.671Z
Learning: Empty useEffect hooks in the VariableElement component (src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx) are intentional placeholders that will be implemented later as they depend on other logic.
Applied to files:
src/renderer/components/_molecules/global-variables-table/selectable-cell.tsx
🧬 Code graph analysis (3)
src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx (6)
src/renderer/store/index.ts (1)
useOpenPLCStore(91-91)src/shared/data/common.ts (1)
baseTypes(34-34)src/types/PLC/open-plc.ts (1)
BaseType(445-445)src/renderer/store/slices/project/validation/variables.ts (1)
arrayValidation(531-531)src/renderer/components/_features/[app]/toast/use-toast.tsx (1)
toast(240-240)src/renderer/components/_atoms/dimensions-modal/index.tsx (1)
DimensionsModal(36-160)
src/renderer/components/_molecules/global-variables-table/selectable-cell.tsx (2)
src/types/PLC/open-plc.ts (2)
PLCGlobalVariable(460-460)PLCVariable(474-474)src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx (1)
GlobalArrayModal(20-194)
src/renderer/components/_organisms/workspace-activity-bar/default.tsx (1)
src/renderer/utils/parse-debug-file.ts (1)
matchVariableWithDebugEntry(125-148)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: build (ubuntu-latest)
- GitHub Check: build (windows-latest)
- GitHub Check: build (macos-latest)
- GitHub Check: test (macos-latest)
- GitHub Check: test (windows-latest)
- GitHub Check: test (ubuntu-latest)
🔇 Additional comments (11)
src/renderer/components/_organisms/workspace-activity-bar/default.tsx (1)
888-888: LGTM! Proper integration with enhanced variable matching.The addition of
v.classas the fourth parameter correctly aligns with the updatedmatchVariableWithDebugEntrysignature, enabling the debugger to distinguish external variables and map them to the correct global entries (CONFIG0__ prefix).src/renderer/utils/debug-tree-builder.ts (2)
47-60: LGTM! Excellent refactoring for centralized path construction.The
buildVariableBasePathhelper consolidates the logic for determining variable paths based on class (external vs. local), eliminating duplication and ensuring consistent handling of the CONFIG0__ vs. RES0__ prefix across the codebase.
86-86: LGTM! Consistent usage of the new helper.All call sites correctly delegate path construction to
buildVariableBasePath, passing the appropriatevariableClasswhen available.Also applies to: 107-107, 142-142, 580-580, 669-669
src/renderer/utils/parse-debug-file.ts (2)
125-148: LGTM! Correct implementation of external variable matching.The extended signature with optional
variableClassparameter and the new branch for external variables correctly implement the mapping to global debug entries (CONFIG0__ prefix), ensuring the debugger resolves external POU variables to the actual global variables.
150-164: LGTM! Clear and focused helper for global variable matching.The new
matchGlobalVariableWithDebugEntryfunction provides a clean API for directly matching global variables with their debug entries using the CONFIG0__ prefix.src/renderer/components/_molecules/global-variables-table/selectable-cell.tsx (4)
68-87: LGTM! Comprehensive library type extraction.The
LibraryTypesdefinition correctly extracts function blocks from both system and user libraries, handling the variation in structure (libraries with POUs arrays vs. single function blocks).Based on learnings, the runtime type checking with
'pous' in userLibraryis appropriate given the dynamic library structure.
205-211: LGTM! Clean integration of array modal.The
GlobalArrayModalcomponent is properly wired with state management and thecloseContainercallback ensures the dropdown closes when the modal is opened.
234-305: LGTM! Enhanced UX with searchable submenus.The multi-submenu structure with search/filter capability significantly improves the user experience for selecting variable types. The implementation correctly:
- Provides per-definition search filtering
- Displays appropriate "No ... found" messages
- Opens the array modal as a menu item
- Resets filters on submenu close
307-357: LGTM! Consistent pattern for library submenus.The library type submenus (system and user) follow the same searchable pattern as the variable type submenus, providing a consistent UI experience. The selection correctly uses 'derived' as the definition for library function blocks.
src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx (2)
155-155: Type suppression comment indicates pending type fix.The
@ts-expect-errorcomment at Line 155 suggests a known type mismatch that the team is tracking. Ensure this is addressed in follow-up work to maintain type safety.Is there an issue tracking the resolution of this type error?
40-64: LGTM! Solid array modal implementation.The component correctly:
- Extracts base types, user data types, and function blocks from libraries
- Initializes state from existing global variables
- Validates dimensions before saving
- Constructs proper array type definitions with base type classification
- Integrates cleanly with the store's
updateVariableactionAlso applies to: 70-81, 130-167
There was a problem hiding this comment.
Pull request overview
This PR adds support for function block and array types to global variables, and fixes the debugger to correctly map external variables to their corresponding global variables (using CONFIG0__ prefix instead of RES0__ prefix).
Changes:
- Enhanced global variables table with function block types (system/user libraries) and array support
- Fixed debugger mapping for external variables to use
CONFIG0__prefix for global variables - Added search/filter functionality to the type selector dropdown
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/renderer/utils/parse-debug-file.ts |
Added handling for external variables to map to global variables with CONFIG0__ prefix |
src/renderer/utils/debug-tree-builder.ts |
Refactored path construction with new buildVariableBasePath helper function |
src/renderer/components/_organisms/workspace-activity-bar/default.tsx |
Updated to pass variable class parameter to matching function |
src/renderer/components/_molecules/global-variables-table/selectable-cell.tsx |
Added library types and array support to type selector with search functionality |
src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx |
New modal component for creating/editing array global variables |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| definition: 'array', | ||
| value: formatArrayName, | ||
| data: { | ||
| // @ts-expect-error - This is a valid operation. This is being fixed. |
There was a problem hiding this comment.
The ts-expect-error comment lacks specificity about what is being fixed and when. Consider adding more context such as a ticket number or a more detailed explanation of the type mismatch.
| // @ts-expect-error - This is a valid operation. This is being fixed. | |
| // @ts-expect-error - `type.data` in the variable schema does not currently declare a `baseType` | |
| // field, but PLC array types expect this shape at runtime. This suppression can be removed once | |
| // the TypeScript definitions for global variable `type.data` are updated to include `baseType`. |
| const handleRearrangeDimensions = (index: number, direction: 'up' | 'down') => { | ||
| if (direction === 'up') { | ||
| if (index === 0) return | ||
| const [removed] = dimensions.splice(index, 1) | ||
| dimensions.splice(index - 1, 0, removed) | ||
| setSelectedInput((index - 1).toString()) | ||
| return | ||
| } | ||
|
|
||
| if (index === dimensions.length - 1) return | ||
| const [removed] = dimensions.splice(index, 1) | ||
| dimensions.splice(index + 1, 0, removed) | ||
| setSelectedInput((index + 1).toString()) |
There was a problem hiding this comment.
The variable name removed is reused in both the 'up' and 'down' branches with the same splice operation. Consider extracting the common logic into a helper function or variable to reduce duplication.
| const handleRearrangeDimensions = (index: number, direction: 'up' | 'down') => { | |
| if (direction === 'up') { | |
| if (index === 0) return | |
| const [removed] = dimensions.splice(index, 1) | |
| dimensions.splice(index - 1, 0, removed) | |
| setSelectedInput((index - 1).toString()) | |
| return | |
| } | |
| if (index === dimensions.length - 1) return | |
| const [removed] = dimensions.splice(index, 1) | |
| dimensions.splice(index + 1, 0, removed) | |
| setSelectedInput((index + 1).toString()) | |
| const moveDimension = (fromIndex: number, toIndex: number) => { | |
| const [removed] = dimensions.splice(fromIndex, 1) | |
| dimensions.splice(toIndex, 0, removed) | |
| setSelectedInput(toIndex.toString()) | |
| } | |
| const handleRearrangeDimensions = (index: number, direction: 'up' | 'down') => { | |
| if (direction === 'up') { | |
| if (index === 0) return | |
| moveDimension(index, index - 1) | |
| return | |
| } | |
| if (index === dimensions.length - 1) return | |
| moveDimension(index, index + 1) |
| onOpenChange={() => setVariableFilters((prev) => ({ ...prev, [scope.definition]: '' }))} | ||
| > | ||
| <PrimitiveDropdown.SubTrigger asChild> | ||
| <div className='relative flex h-8 w-full cursor-pointer items-center justify-center py-1 outline-none hover:bg-neutral-100 dark:hover:bg-neutral-900'> |
There was a problem hiding this comment.
The className string on this line is duplicated across multiple similar elements in this file (lines 243, 312). Consider extracting this repeated style pattern into a constant or using a shared component to improve maintainability.
- Fix handleRearrangeDimensions to use immutable state updates (create copy before using splice instead of mutating state directly) - Simplify isBaseType check using includes() instead of forEach loop Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
@src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx:
- Around line 83-86: handleAddDimension has a stale closure: it uses
dimensions.length (captured) immediately after calling setDimensions, so
selectedInput gets the old index; change handleAddDimension to use the
functional updater form for setDimensions (e.g., setDimensions(prev => { const
newIndex = prev.length; const next = [...prev, ''];
setSelectedInput(newIndex.toString()); return next; })) so selectedInput is
derived from prev.length and matches the newly added dimension.
- Around line 146-160: The code currently sets the inner type definition based
only on baseTypes (isBaseType) which misclassifies function block types; update
the logic where updateVariable is called (the type.data.baseType block using
typeValue and isBaseType) to first check membership in systemFunctionBlocks or
userFunctionBlocks and, if matched, set definition to 'derived', else if
isBaseType set 'base-type', otherwise set 'user-data-type' (preserving value:
typeValue); keep existing surrounding structure (formatArrayName, variableRow,
scope: 'global') unchanged.
🧹 Nitpick comments (1)
src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx (1)
156-160: Consider removing or documenting the@ts-expect-errorsuppression.The comment notes "This is being fixed" but doesn't indicate the tracking issue or timeline. If this is a known type mismatch with a planned fix, consider linking to an issue for traceability.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-16T14:31:51.786Z
Learnt from: vmleroy
Repo: Autonomy-Logic/openplc-editor PR: 265
File: src/renderer/components/_atoms/graphical-editor/ladder/block.tsx:149-153
Timestamp: 2025-06-16T14:31:51.786Z
Learning: In src/renderer/components/_atoms/graphical-editor/ladder/block.tsx, the libraries.system data is dynamic and imported from JSON with varying structure, making proper static typing infeasible. The ts-expect-error and eslint-disable comments are justified when dynamically probing the structure at runtime.
Applied to files:
src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx
📚 Learning: 2025-07-04T15:13:57.599Z
Learnt from: vmleroy
Repo: Autonomy-Logic/openplc-editor PR: 274
File: src/types/PLC/devices/configuration.ts:0-0
Timestamp: 2025-07-04T15:13:57.599Z
Learning: In src/types/PLC/devices/configuration.ts, the team prefers to handle IP address validation at the local store level when passing values from frontend to backend, rather than adding additional validation at the schema level.
Applied to files:
src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: build (macos-latest)
- GitHub Check: build (ubuntu-latest)
- GitHub Check: build (windows-latest)
- GitHub Check: test (windows-latest)
- GitHub Check: test (macos-latest)
🔇 Additional comments (1)
src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx (1)
20-195: Overall: Well-structured modal component.The component follows established patterns in the codebase, properly handles store integration, and includes appropriate validation. The dimension manipulation handlers are comprehensive. Based on learnings, the dynamic type handling for libraries data is acceptable given the varying JSON structure.
| const handleAddDimension = () => { | ||
| setDimensions((prev) => [...prev, '']) | ||
| setSelectedInput(dimensions.length.toString()) | ||
| } |
There was a problem hiding this comment.
Stale closure bug in handleAddDimension.
dimensions.length captures the value at the time the handler was created, not after the state update. Since setDimensions is asynchronous, selectedInput will be set to the old length rather than the new dimension's index.
🐛 Proposed fix using functional update
const handleAddDimension = () => {
- setDimensions((prev) => [...prev, ''])
- setSelectedInput(dimensions.length.toString())
+ setDimensions((prev) => {
+ setSelectedInput(prev.length.toString())
+ return [...prev, '']
+ })
}Alternatively, use a ref or compute the new length explicitly:
const handleAddDimension = () => {
+ const newIndex = dimensions.length
setDimensions((prev) => [...prev, ''])
- setSelectedInput(dimensions.length.toString())
+ setSelectedInput(newIndex.toString())
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleAddDimension = () => { | |
| setDimensions((prev) => [...prev, '']) | |
| setSelectedInput(dimensions.length.toString()) | |
| } | |
| const handleAddDimension = () => { | |
| const newIndex = dimensions.length | |
| setDimensions((prev) => [...prev, '']) | |
| setSelectedInput(newIndex.toString()) | |
| } |
🤖 Prompt for AI Agents
In
@src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx
around lines 83 - 86, handleAddDimension has a stale closure: it uses
dimensions.length (captured) immediately after calling setDimensions, so
selectedInput gets the old index; change handleAddDimension to use the
functional updater form for setDimensions (e.g., setDimensions(prev => { const
newIndex = prev.length; const next = [...prev, ''];
setSelectedInput(newIndex.toString()); return next; })) so selectedInput is
derived from prev.length and matches the newly added dimension.
| const isBaseType = baseTypes.includes(typeValue) | ||
|
|
||
| updateVariable({ | ||
| scope: 'global', | ||
| rowId: variableRow, | ||
| data: { | ||
| type: { | ||
| definition: 'array', | ||
| value: formatArrayName, | ||
| data: { | ||
| // @ts-expect-error - This is a valid operation. This is being fixed. | ||
| baseType: { | ||
| definition: isBaseType ? 'base-type' : 'user-data-type', | ||
| value: typeValue, | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for baseType definition patterns to understand how function blocks are handled
rg -n "definition.*base-type|definition.*user-data-type|definition.*derived" --type ts -B2 -A2Repository: Autonomy-Logic/openplc-editor
Length of output: 50386
🏁 Script executed:
# Look for how LibraryTypes or function block types are used in array handling
rg -n "LibraryTypes|userFunctionBlocks|systemFunctionBlocks" --type ts src/renderer/components/_molecules/global-variables-table/ -A3 -B3Repository: Autonomy-Logic/openplc-editor
Length of output: 6674
🏁 Script executed:
# Find the baseTypes definition to understand what types are considered base types
rg -n "baseTypes|baseTypeSchema" --type ts src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx -B5 -A5Repository: Autonomy-Logic/openplc-editor
Length of output: 1765
🏁 Script executed:
# Check for array variable type handling in POU context (if it exists)
fd "array" --type f src/renderer/components/_molecules/ | head -20Repository: Autonomy-Logic/openplc-editor
Length of output: 318
🏁 Script executed:
# Search for how variable types are classified in the broader codebase
rg -n "typeValue.*definition|isBaseType" --type ts -C4Repository: Autonomy-Logic/openplc-editor
Length of output: 8610
🏁 Script executed:
# Check if there are tests or other usages that show function blocks in arrays
rg -n "derived.*function-block|function-block.*derived" --type ts -B2 -A2 | head -30Repository: Autonomy-Logic/openplc-editor
Length of output: 55
🏁 Script executed:
# Verify the structure of LibraryTypes selection in array-modal
sed -n '180,220p' src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsxRepository: Autonomy-Logic/openplc-editor
Length of output: 563
Handle function block type classification separately.
The logic classifies typeValue as either 'base-type' or 'user-data-type' based solely on whether it exists in baseTypes. However, function blocks selected from LibraryTypes (which are uppercase) will never match baseTypes (lowercase), causing them to be incorrectly assigned definition: 'user-data-type'.
Function block types should use definition: 'derived' instead. Add a check: if typeValue matches any function block from systemFunctionBlocks or userFunctionBlocks, assign 'derived'. Only unmatched non-base types should fall back to 'user-data-type' (for user-defined structs).
🤖 Prompt for AI Agents
In
@src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx
around lines 146 - 160, The code currently sets the inner type definition based
only on baseTypes (isBaseType) which misclassifies function block types; update
the logic where updateVariable is called (the type.data.baseType block using
typeValue and isBaseType) to first check membership in systemFunctionBlocks or
userFunctionBlocks and, if matched, set definition to 'derived', else if
isBaseType set 'base-type', otherwise set 'user-data-type' (preserving value:
typeValue); keep existing surrounding structure (formatArrayName, variableRow,
scope: 'global') unchanged.
Summary
Changes
1. Global Variables Table Enhancements
GlobalArrayModalcomponent2. Debugger External → Global Variable Mapping
When forcing or viewing an external variable in a POU, the debugger now correctly maps it to the corresponding global variable (
CONFIG0__prefix) instead of looking for a local POU variable (RES0__prefix).Files changed:
src/renderer/utils/parse-debug-file.ts- AddedvariableClassparameter to handle external variablessrc/renderer/utils/debug-tree-builder.ts- AddedbuildVariableBasePathhelper for correct path constructionsrc/renderer/components/_organisms/workspace-activity-bar/default.tsx- Pass variable class to matching functionsrc/renderer/components/_molecules/global-variables-table/- New array modal and enhanced type selectorWhy This Matters
Previously:
Now:
CONFIG0__VARNAMEin debug.cRelated
Requires xml2st fix: Autonomy-Logic/xml2st#24
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.