refactor(platform): extract organization switcher into dedicated nav button#1640
Conversation
…button Move org list, switching, and deletion out of the user-button dropdown and organization settings page into a standalone OrganizationButton fed by a shared OrganizationListPanel. Adds remove-rule confirmation copy across governance editors and aligns en/de/fr message catalogs.
📝 WalkthroughWalkthroughThis PR refactors organization management by introducing an Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@services/platform/app/components/organization-button.tsx`:
- Around line 51-63: The icon-only org switcher trigger (the JSX stored in
triggerContent used by DropdownMenu) lacks an accessible name when label is
falsy; add a translated aria-label on the actual trigger element (the <div>
returned as triggerContent) whenever label is absent using the project's i18n
helper (e.g., useTranslations or t) instead of hardcoded English, and apply the
same change to the second icon-only trigger around the code at the other
occurrence (lines 76-95) so screen readers receive an accessible name separate
from the tooltip.
In `@services/platform/app/components/ui/navigation/mobile-navigation.tsx`:
- Around line 165-168: The bottom button container currently uses "py-2" which
yields ~36px height for UserButton and OrganizationButton; update the
container's tailwind padding from "py-2" to "py-3" (or a value that ensures
≥44px total height) in the div that wraps UserButton and OrganizationButton so
both components meet the 44×44 CSS pixel mobile touch target; verify the final
computed height and adjust to "py-4" only if needed to guarantee the minimum
touch size.
In `@services/platform/app/components/user-button.tsx`:
- Around line 237-242: The current mapping forces any non-en/de locale to 'fr';
change currentLocaleValue to derive the base locale (const base =
locale.split('-')[0]) and then map explicitly only for supported options: if
base === 'en' -> 'en', else if base === 'de' -> 'de', else if base === 'fr' ->
'fr', otherwise use base (or an empty/unsupported value) so unknown locales
(e.g., 'es', 'it') are not incorrectly treated as French; apply the same change
to the other occurrence around the currentLocaleValue logic at the 252-260
block.
In `@services/platform/app/features/chat/components/chat-history-sidebar.tsx`:
- Around line 426-427: The added rotation/transition classes in
chat-history-sidebar.tsx need motion-reduce fallbacks: update the class strings
that include 'transition-transform duration-300 ease-out' and the conditional
'-rotate-90' (around the archivedExpanded toggle) to also include Tailwind
motion-reduce variants (e.g., prefacing overrides with 'motion-reduce:') so
animations are disabled/reduced for users who prefer reduced motion; apply the
same change to the other occurrence mentioned (the class group at the 444-447
block) so both the open/close rotation and any duration/transform transitions
respect prefers-reduced-motion.
- Around line 442-450: The collapsed archived section currently only sets
aria-hidden based on archivedExpanded, but its descendant interactive elements
(buttons/links) remain tabbable; update the container div and rendering logic
around archivedExpanded so the collapsed state is truly non-interactive: when
archivedExpanded is false, either remove the archived panel from the DOM (only
render when true) or mark the container with inert (or hidden) plus aria-hidden
and ensure descendant interactive controls are not focusable (e.g., set
tabIndex=-1 or avoid rendering Button components) — change the container div
(the element using archivedExpanded and aria-hidden) and/or the descendant
button/link render paths so that when !archivedExpanded they cannot receive
focus or pointer events.
In
`@services/platform/app/features/organization/components/organization-list-panel.tsx`:
- Around line 179-197: The delete button is hidden by default and has too small
a hit area for touch devices; in organization-list-panel.tsx update the button's
className so it is always visible on touch/small screens and has a larger hit
target: replace the current opacity and padding utilities (e.g., 'opacity-0
group-hover/org:opacity-100 ... p-1') with responsive variants such that
small/coarse pointers show the button (for example use 'sm:opacity-0
sm:group-hover/org:opacity-100 opacity-100' or equivalent
media-query/coarse-pointer Tailwind variants) and increase padding to meet
touch-target guidance (e.g., 'p-3 sm:p-1' or otherwise ensure 44×44 on mobile
and >=24×24 elsewhere); keep the existing aria-label, onClick handler and Trash2
icon (references: setDeleteTarget, org.organizationId, org.name, Trash2) so
keyboard/focus behavior remains intact.
In
`@services/platform/app/features/settings/governance/components/chat-filter-config.tsx`:
- Around line 168-176: In confirmDeleteCategory update the editor index handling
so it doesn't become off-by-one when removing a category before the currently
edited one: after computing index and next, call setEditorIndex with a function
that if prev is null returns null, if prev === index returns null (we deleted
the edited item), if prev > index returns prev - 1 (shift editor down one),
otherwise returns prev; keep the rest of the logic (setCategories,
setDeletingIndex, saveWith(buildConfig(...))) unchanged and reference the
confirmDeleteCategory, setEditorIndex, deletingIndex and categories identifiers
when making the change.
In
`@services/platform/app/features/settings/governance/components/guardrails-overview.tsx`:
- Around line 82-96: The disabled state currently hides the explanation inside a
non-focusable Tooltip on the Badge; change the JSX in the guardrails-overview
component so that when enabled is false you render the disabledReason visibly
(e.g., display the Badge plus an adjacent text element like a <span> or <p>
showing disabledReason) instead of tooltip-only, or if you must keep the Tooltip
keep its trigger keyboard-focusable and associated with the description (make
the Badge a focusable element/button or add tabIndex={0} and aria-describedby
linking to the tooltip content). Update the conditional that renders
Tooltip/Badge around enabled to render an accessible visible copy of
disabledReason (reference variables: enabled, disabledReason and components
Tooltip/Badge in guardrails-overview.tsx).
🪄 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: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 8dc33dbf-3d19-42dd-bd73-a221c435da83
📒 Files selected for processing (20)
services/platform/app/components/organization-button.tsxservices/platform/app/components/ui/navigation/mobile-navigation.tsxservices/platform/app/components/ui/navigation/navigation.tsxservices/platform/app/components/user-button.tsxservices/platform/app/features/chat/components/chat-history-sidebar.tsxservices/platform/app/features/chat/components/model-selector.tsxservices/platform/app/features/organization/components/organization-list-panel.tsxservices/platform/app/features/settings/governance/components/budget-editor.tsxservices/platform/app/features/settings/governance/components/chat-filter-config.tsxservices/platform/app/features/settings/governance/components/default-model-editor.tsxservices/platform/app/features/settings/governance/components/feature-flags-editor.tsxservices/platform/app/features/settings/governance/components/guardrails-overview.tsxservices/platform/app/features/settings/governance/components/model-access-editor.tsxservices/platform/app/features/settings/governance/components/moderation-provider-config.tsxservices/platform/app/features/settings/organization/components/organization-settings.tsxservices/platform/app/hooks/use-navigation-items.tsservices/platform/app/routes/dashboard/$id/automations/$amId.tsxservices/platform/messages/de.jsonservices/platform/messages/en.jsonservices/platform/messages/fr.json
| const triggerContent = ( | ||
| <div | ||
| className={cn( | ||
| 'relative flex items-center rounded-lg transition-colors hover:bg-muted cursor-pointer', | ||
| label ? 'gap-3 px-3 py-2 w-full' : 'justify-center p-2', | ||
| )} | ||
| > | ||
| <Building2 className="text-muted-foreground size-5 shrink-0" /> | ||
| {label && ( | ||
| <span className="text-sm leading-none font-medium">{label}</span> | ||
| )} | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
Add an accessible name to the icon-only org switcher trigger.
When label is absent, this control is just an icon plus a tooltip. The tooltip text does not give the trigger its accessible name, so screen readers can end up announcing an unlabeled button/menu trigger. Please thread a translated aria-label onto the actual trigger element used by DropdownMenu.
As per coding guidelines, "Every icon-only button must have a translated aria-label — never hardcode English in ARIA."
Also applies to: 76-95
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/app/components/organization-button.tsx` around lines 51 -
63, The icon-only org switcher trigger (the JSX stored in triggerContent used by
DropdownMenu) lacks an accessible name when label is falsy; add a translated
aria-label on the actual trigger element (the <div> returned as triggerContent)
whenever label is absent using the project's i18n helper (e.g., useTranslations
or t) instead of hardcoded English, and apply the same change to the second
icon-only trigger around the code at the other occurrence (lines 76-95) so
screen readers receive an accessible name separate from the tooltip.
| <div className="border-border flex flex-shrink-0 flex-col gap-1 border-t px-4 py-2"> | ||
| <UserButton label={tNav('account')} /> | ||
| <OrganizationButton label={tNav('orgSwitcher.label')} /> | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check UserButton and OrganizationButton for size/padding that affects touch targets
rg -n "className" services/platform/app/components/user-button.tsx | head -10
rg -n "className" services/platform/app/components/organization-button.tsx | head -10Repository: tale-project/tale
Length of output: 1182
🏁 Script executed:
cat -n services/platform/app/components/user-button.tsxRepository: tale-project/tale
Length of output: 14163
🏁 Script executed:
cat -n services/platform/app/components/organization-button.tsxRepository: tale-project/tale
Length of output: 3602
🏁 Script executed:
sed -n '155,180p' services/platform/app/components/ui/navigation/mobile-navigation.tsxRepository: tale-project/tale
Length of output: 713
Increase button padding to meet 44×44 CSS pixel mobile touch target requirement.
The UserButton and OrganizationButton components render with approximately 36px height (16px vertical padding + 20px icon), falling short of the 44×44 CSS pixel minimum for mobile touch targets required by WCAG 2.1 Level AA. Increase py-2 to py-3 (12px top/bottom) or higher to reach the minimum touch target size.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/app/components/ui/navigation/mobile-navigation.tsx` around
lines 165 - 168, The bottom button container currently uses "py-2" which yields
~36px height for UserButton and OrganizationButton; update the container's
tailwind padding from "py-2" to "py-3" (or a value that ensures ≥44px total
height) in the div that wraps UserButton and OrganizationButton so both
components meet the 44×44 CSS pixel mobile touch target; verify the final
computed height and adjust to "py-4" only if needed to guarantee the minimum
touch size.
| const currentLocaleValue = | ||
| locale === 'en' || locale.startsWith('en-') | ||
| ? 'en' | ||
| : locale.startsWith('de') | ||
| ? 'de' | ||
| : 'fr'; |
There was a problem hiding this comment.
Don't default unknown locales to French.
This normalization maps every non-en/de locale to fr, so values like es, it, or any other valid stored locale will render the French option as selected even though French is not active.
💡 Suggested fix
- const currentLocaleValue =
- locale === 'en' || locale.startsWith('en-')
- ? 'en'
- : locale.startsWith('de')
- ? 'de'
- : 'fr';
+ const currentLocaleValue = locale.startsWith('de')
+ ? 'de'
+ : locale.startsWith('fr')
+ ? 'fr'
+ : 'en';Also applies to: 252-260
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/app/components/user-button.tsx` around lines 237 - 242, The
current mapping forces any non-en/de locale to 'fr'; change currentLocaleValue
to derive the base locale (const base = locale.split('-')[0]) and then map
explicitly only for supported options: if base === 'en' -> 'en', else if base
=== 'de' -> 'de', else if base === 'fr' -> 'fr', otherwise use base (or an
empty/unsupported value) so unknown locales (e.g., 'es', 'it') are not
incorrectly treated as French; apply the same change to the other occurrence
around the currentLocaleValue logic at the 252-260 block.
| 'size-3.5 shrink-0 transition-transform duration-300 ease-out', | ||
| !archivedExpanded && '-rotate-90', |
There was a problem hiding this comment.
Add reduced-motion fallbacks for the new animations.
These new transitions should include motion-reduce: variants so users with reduced-motion preference aren’t forced through animations.
Suggested fix
- 'size-3.5 shrink-0 transition-transform duration-300 ease-out',
+ 'size-3.5 shrink-0 transition-transform duration-300 ease-out motion-reduce:transition-none',
...
- 'grid transition-[grid-template-rows,opacity] duration-300 ease-out',
+ 'grid transition-[grid-template-rows,opacity] duration-300 ease-out motion-reduce:transition-none',As per coding guidelines: "Respect prefers-reduced-motion: reduce on every animation. Use the motion-reduce: Tailwind prefix for CSS overrides."
Also applies to: 444-447
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/app/features/chat/components/chat-history-sidebar.tsx`
around lines 426 - 427, The added rotation/transition classes in
chat-history-sidebar.tsx need motion-reduce fallbacks: update the class strings
that include 'transition-transform duration-300 ease-out' and the conditional
'-rotate-90' (around the archivedExpanded toggle) to also include Tailwind
motion-reduce variants (e.g., prefacing overrides with 'motion-reduce:') so
animations are disabled/reduced for users who prefer reduced motion; apply the
same change to the other occurrence mentioned (the class group at the 444-447
block) so both the open/close rotation and any duration/transform transitions
respect prefers-reduced-motion.
| <div | ||
| className={cn( | ||
| 'grid transition-[grid-template-rows,opacity] duration-300 ease-out', | ||
| archivedExpanded | ||
| ? 'grid-rows-[1fr] opacity-100' | ||
| : 'grid-rows-[0fr] opacity-0', | ||
| )} | ||
| aria-hidden={!archivedExpanded} | ||
| > |
There was a problem hiding this comment.
Collapsed archived section can still expose hidden interactive controls to keyboard users.
When collapsed, this region is only aria-hidden; its descendant buttons can still be tabbable. Please make the collapsed state non-interactive and non-focusable.
Suggested fix
<div
className={cn(
'grid transition-[grid-template-rows,opacity] duration-300 ease-out',
archivedExpanded
? 'grid-rows-[1fr] opacity-100'
- : 'grid-rows-[0fr] opacity-0',
+ : 'grid-rows-[0fr] opacity-0 pointer-events-none',
)}
aria-hidden={!archivedExpanded}
>
<Stack
gap={1}
- className="max-h-64 min-h-0 overflow-y-auto pt-1 pb-2"
+ className={cn(
+ 'max-h-64 min-h-0 overflow-y-auto pt-1 pb-2',
+ !archivedExpanded && 'invisible',
+ )}
>As per coding guidelines: "Everything interactive must be keyboard-reachable." and "Accessibility is Level AA, not a nice-to-have."
Also applies to: 451-492
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/app/features/chat/components/chat-history-sidebar.tsx`
around lines 442 - 450, The collapsed archived section currently only sets
aria-hidden based on archivedExpanded, but its descendant interactive elements
(buttons/links) remain tabbable; update the container div and rendering logic
around archivedExpanded so the collapsed state is truly non-interactive: when
archivedExpanded is false, either remove the archived panel from the DOM (only
render when true) or mark the container with inert (or hidden) plus aria-hidden
and ensure descendant interactive controls are not focusable (e.g., set
tabIndex=-1 or avoid rendering Button components) — change the container div
(the element using archivedExpanded and aria-hidden) and/or the descendant
button/link render paths so that when !archivedExpanded they cannot receive
focus or pointer events.
| <button | ||
| type="button" | ||
| aria-label={tSettings('organization.deleteAriaLabel', { | ||
| name: org.name, | ||
| })} | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| setDeleteTarget({ | ||
| organizationId: org.organizationId, | ||
| name: org.name, | ||
| }); | ||
| }} | ||
| className={cn( | ||
| 'absolute top-1/2 right-2 -translate-y-1/2 rounded p-1 transition-opacity', | ||
| 'text-muted-foreground hover:text-destructive hover:bg-destructive/10', | ||
| 'opacity-0 group-hover/org:opacity-100 focus-visible:opacity-100', | ||
| )} | ||
| > | ||
| <Trash2 className="size-3.5" /> |
There was a problem hiding this comment.
Make the delete action visible and tappable on touch devices.
The trash button stays hidden unless the row is hovered or receives focus, so it is effectively undiscoverable on touch screens. Its p-1 hit area is also below the repo's minimum touch-target guidance for destructive controls.
As per coding guidelines, "Everything interactive must be keyboard-reachable. Test with Tab and Shift+Tab before merging." and "Minimum 24×24 CSS pixel touch target (44×44 on mobile)."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@services/platform/app/features/organization/components/organization-list-panel.tsx`
around lines 179 - 197, The delete button is hidden by default and has too small
a hit area for touch devices; in organization-list-panel.tsx update the button's
className so it is always visible on touch/small screens and has a larger hit
target: replace the current opacity and padding utilities (e.g., 'opacity-0
group-hover/org:opacity-100 ... p-1') with responsive variants such that
small/coarse pointers show the button (for example use 'sm:opacity-0
sm:group-hover/org:opacity-100 opacity-100' or equivalent
media-query/coarse-pointer Tailwind variants) and increase padding to meet
touch-target guidance (e.g., 'p-3 sm:p-1' or otherwise ensure 44×44 on mobile
and >=24×24 elsewhere); keep the existing aria-label, onClick handler and Trash2
icon (references: setDeleteTarget, org.organizationId, org.name, Trash2) so
keyboard/focus behavior remains intact.
| const confirmDeleteCategory = useCallback(() => { | ||
| if (deletingIndex === null) return; | ||
| const index = deletingIndex; | ||
| const next = categories.filter((_, i) => i !== index); | ||
| setCategories(next); | ||
| setEditorIndex((prev) => (prev === index ? null : prev)); | ||
| setDeletingIndex(null); | ||
| void saveWith(buildConfig({ categories: next })); | ||
| }, [buildConfig, categories, deletingIndex, saveWith]); |
There was a problem hiding this comment.
Adjust editor index when deleting an earlier row
At Line 173, editorIndex is only cleared on exact match. If a category before the currently edited one is deleted, the edited index becomes off-by-one and can point to the wrong category.
Suggested fix
- setEditorIndex((prev) => (prev === index ? null : prev));
+ setEditorIndex((prev) => {
+ if (prev === null || prev === 'new') return prev;
+ if (prev === index) return null;
+ return prev > index ? prev - 1 : prev;
+ });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@services/platform/app/features/settings/governance/components/chat-filter-config.tsx`
around lines 168 - 176, In confirmDeleteCategory update the editor index
handling so it doesn't become off-by-one when removing a category before the
currently edited one: after computing index and next, call setEditorIndex with a
function that if prev is null returns null, if prev === index returns null (we
deleted the edited item), if prev > index returns prev - 1 (shift editor down
one), otherwise returns prev; keep the rest of the logic (setCategories,
setDeletingIndex, saveWith(buildConfig(...))) unchanged and reference the
confirmDeleteCategory, setEditorIndex, deletingIndex and categories identifiers
when making the change.
| {enabled ? ( | ||
| <ul className="text-xs"> | ||
| {details.map((detail) => ( | ||
| <li key={detail} className="py-0.5"> | ||
| {detail} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| ) : ( | ||
| <Tooltip content={disabledReason}> | ||
| <Badge variant="outline" icon={Info} className="mt-auto cursor-help"> | ||
| {tCommon('status.disabled')} | ||
| </Badge> | ||
| </Tooltip> | ||
| )} |
There was a problem hiding this comment.
Keep the disabled reason visible instead of tooltip-only.
This change removes the inline explanation and leaves it behind a tooltip on a non-interactive badge. That makes the reason easy to miss for keyboard and touch users. Please keep the disabled copy visible in the card, or make the trigger explicitly focusable and associated with the description.
💡 Suggested fix
- ) : (
- <Tooltip content={disabledReason}>
- <Badge variant="outline" icon={Info} className="mt-auto cursor-help">
- {tCommon('status.disabled')}
- </Badge>
- </Tooltip>
- )}
+ ) : (
+ <div className="space-y-2">
+ <Badge variant="outline" icon={Info} className="mt-auto">
+ {tCommon('status.disabled')}
+ </Badge>
+ <p className="text-muted-foreground text-xs">{disabledReason}</p>
+ </div>
+ )}📝 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.
| {enabled ? ( | |
| <ul className="text-xs"> | |
| {details.map((detail) => ( | |
| <li key={detail} className="py-0.5"> | |
| {detail} | |
| </li> | |
| ))} | |
| </ul> | |
| ) : ( | |
| <Tooltip content={disabledReason}> | |
| <Badge variant="outline" icon={Info} className="mt-auto cursor-help"> | |
| {tCommon('status.disabled')} | |
| </Badge> | |
| </Tooltip> | |
| )} | |
| {enabled ? ( | |
| <ul className="text-xs"> | |
| {details.map((detail) => ( | |
| <li key={detail} className="py-0.5"> | |
| {detail} | |
| </li> | |
| ))} | |
| </ul> | |
| ) : ( | |
| <div className="space-y-2"> | |
| <Badge variant="outline" icon={Info} className="mt-auto"> | |
| {tCommon('status.disabled')} | |
| </Badge> | |
| <p className="text-muted-foreground text-xs">{disabledReason}</p> | |
| </div> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@services/platform/app/features/settings/governance/components/guardrails-overview.tsx`
around lines 82 - 96, The disabled state currently hides the explanation inside
a non-focusable Tooltip on the Badge; change the JSX in the guardrails-overview
component so that when enabled is false you render the disabledReason visibly
(e.g., display the Badge plus an adjacent text element like a <span> or <p>
showing disabledReason) instead of tooltip-only, or if you must keep the Tooltip
keep its trigger keyboard-focusable and associated with the description (make
the Badge a focusable element/button or add tabIndex={0} and aria-describedby
linking to the tooltip content). Update the conditional that renders
Tooltip/Badge around enabled to render an accessible visible copy of
disabledReason (reference variables: enabled, disabledReason and components
Tooltip/Badge in guardrails-overview.tsx).
…action Removes auth.userButton.settings, settings.organization.switchAction, currentBadge, and roleLabel from en/de/fr — all callers were deleted in the user-button slim-down. Aligns user-button test mocks with the new key set so the i18n usage check passes.
- Add aria-label to icon-only org switcher trigger so screen readers get a name beyond the tooltip. - Bump mobile nav button container to py-3 so user/org buttons clear the 44×44 touch-target floor on mobile. - Default unsupported locales to English instead of French in user-button. - Add motion-reduce variants to archived-section animations and mark the collapsed pane as inert + pointer-events-none so descendants leave the tab order. - Always show the org-list delete button on touch (sm:hover-only on desktop) and grow it to a 44×44 touch target on mobile. - Shift editorIndex when an earlier category is removed in chat-filter-config so the editor stays bound to the right row. - Surface guardrail disabledReason inline alongside the Disabled badge instead of hiding it behind a tooltip on a non-focusable trigger.
Lint requires a real <button> over role="button"; switch the trigger to button so screen readers and keyboards still get a labeled control.
Summary
OrganizationButtonand sharedOrganizationListPanelso org switching/deletion lives in the nav rail instead of the user-button dropdown and the organization settings page.user-buttonandorganization-settingsto remove the org list rendering, switching, and delete flow they no longer own.en/de/frmessage catalogs in parity.Test plan
Summary by CodeRabbit
New Features
Improvements