Implement Initial agent chat API - #230
Rashmika998 merged 5 commits into
Conversation
Add react-markdown dependency to support rendering Markdown in the chat UI. Introduce a new API hook (src/api/usePostConversations.ts) to POST conversation messages. Update ChatMessageBubble to use markdown rendering and adjust AddProductModal test accordingly. Lockfile was updated by the package manager.
Introduce ConversationRequest/Response types and wire the POST /projects/:projectId/conversations flow into the UI. DescribeIssuePage now builds envProducts from deployments, posts the initial message to the conversations API, shows a loading spinner, and navigates to the chat page with the API response (or shows an error banner on failure). NoveraChatPage consumes the conversation response to seed initial messages and set a showCreateCaseAction flag; ChatMessageList forwards create-case handlers and loading state to message bubbles. Tests and component mocks updated to reflect new props and UI changes.
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (9)
📝 WalkthroughWalkthroughAdds a conversation API hook and request/response types, integrates async conversation submission on DescribeIssuePage, renders bot messages as Markdown with a create-case action in chat components, adds react-markdown dependency, and updates related tests. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant DescribeIssuePage as DescribeIssuePage
participant PostHook as usePostConversations
participant Backend as Backend API
participant NoveraChatPage as NoveraChatPage
participant ChatList as ChatMessageList
participant ChatBubble as ChatMessageBubble
User->>DescribeIssuePage: submit issue (message, envProducts, region, tier)
DescribeIssuePage->>PostHook: mutateAsync(projectId, payload)
PostHook->>Backend: POST /projects/{projectId}/conversations
Backend-->>PostHook: ConversationResponse
PostHook-->>DescribeIssuePage: resolve(response)
alt Success
DescribeIssuePage->>NoveraChatPage: navigate with conversationResponse
NoveraChatPage->>ChatList: render messages (onCreateCase, isCreateCaseLoading)
ChatList->>ChatBubble: render each message
ChatBubble->>ChatBubble: render bot message via ReactMarkdown
ChatBubble-->>User: display markdown + "Create Case" action (if applicable)
else Error
DescribeIssuePage-->>User: show error banner
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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 |
When handling a describe-issue API response, stop prepending the botWelcome message. The change updates NoveraChatPage to return only the user message and bot response (no welcome), preventing an extra welcome message from appearing in this flow.
Add disabled={isSubmitting} to the toolbar component in DescribeIssuePage.tsx to prevent user interaction during form submission and help avoid duplicate submissions. The change keeps existing onSubmitKeyDown behavior while ensuring the toolbar is inactive while isSubmitting is true.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (8)
apps/customer-portal/webapp/src/components/support/novera-ai-assistant/novera-chat-page/ChatMessageBubble.tsx (2)
36-38: Prefer the namedComponentsexport over the verbose indexed access type.♻️ Suggested refactor
+import type { Components } from "react-markdown"; -const markdownComponents: React.ComponentProps< - typeof ReactMarkdown ->["components"] = { +const markdownComponents: Components = {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/components/support/novera-ai-assistant/novera-chat-page/ChatMessageBubble.tsx` around lines 36 - 38, Replace the verbose indexed access type React.ComponentProps<typeof ReactMarkdown>["components"] with the named export Components from ReactMarkdown: locate the markdownComponents declaration and change its type to ReactMarkdown.Components (or import { Components } from "react-markdown" and use Components) so the variable markdownComponents uses the named Components type for clarity and maintainability.
36-91: Consider addingcode/prerenderers for AI-generated code blocks.The current
markdownComponentscovers headings, paragraphs, lists, and links, but omitscodeandpre. If the AI returns fenced code blocks (likely for technical support), they'll render in unstyled browser defaults. Since the project already includesprismjs, adding a styled code block renderer would complete the markdown surface.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/components/support/novera-ai-assistant/novera-chat-page/ChatMessageBubble.tsx` around lines 36 - 91, markdownComponents currently lacks renderers for fenced code blocks; update the markdownComponents object (used with ReactMarkdown) to include handlers for code and pre that render styled, syntax-highlighted blocks (e.g., use prismjs highlighting or a project-standard SyntaxHighlighter) and ensure language classNames from node.properties.className are parsed to pass the language to the highlighter; implement the code renderer to render inline code as a styled <code> and block code inside a styled <pre> container with proper aria and copy/line-wrap behavior consistent with the app's UI.apps/customer-portal/webapp/src/models/responses.ts (1)
645-658: Strengthen theunknownfields when the backend schema is stabilised.
intent,slotState,actions, andresolvedare allunknown. This is safe for now, butactionsis already consumed via a non-null check inNoveraChatPage.tsx(line 105) to toggleshowCreateCaseAction. Once the backend contract is defined, consider replacing these with concrete types (or at leastRecord<string, unknown>/null-able shapes) to improve discoverability and catch drift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/models/responses.ts` around lines 645 - 658, The ConversationResponse interface currently types intent, slotState, actions, and resolved as unknown; update these fields to stronger, backend-aligned types (or at minimum use Record<string, unknown> | null or specific unions) so consumers can safely access properties—specifically change actions to a nullable object type (e.g., Record<string, unknown> | null) to match the non-null checks in NoveraChatPage.tsx and replace intent, slotState, and resolved with concrete shapes or nullable records once the backend schema is stabilized.apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx (1)
55-60: Consider importingConversationResponseinstead of an inline partial type.The inline type duplicates a subset of
ConversationResponsefromresponses.ts. If the interface evolves (e.g.,actionsis given a concrete type), this cast silently falls behind.♻️ Suggested refactor
+import type { ConversationResponse } from "@models/responses"; - const navState = location.state as { - initialUserMessage?: string; - conversationResponse?: { message: string; actions: unknown }; - } | null; + const navState = location.state as { + initialUserMessage?: string; + conversationResponse?: ConversationResponse; + } | null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx` around lines 55 - 60, Replace the inline partial type used for location.state with the canonical ConversationResponse type: import ConversationResponse from the module that exports it (the same module where responses.ts defines it) and change the cast on navState to use conversationResponse?: ConversationResponse (keeping initialUserMessage?: string) so the component (NoveraChatPage / navState, initialUserMessage, conversationResponse) stays in sync when ConversationResponse evolves.apps/customer-portal/webapp/src/models/requests.ts (1)
47-61:ConversationRequestandCaseClassificationRequestshare three of four fields.Only the first field differs (
messagevschatHistory). Consider extracting the shared fields into a base interface to avoid drift:♻️ Suggested refactor
+/** Shared context fields for AI-related requests. */ +interface AiRequestContext { + envProducts: Record<string, string[]>; + region: string; + tier: string; +} + // Request body for case classification. -export interface CaseClassificationRequest { +export interface CaseClassificationRequest extends AiRequestContext { chatHistory: string; - envProducts: Record<string, string[]>; - region: string; - tier: string; } // Request body for POST /projects/:projectId/conversations (Novera chat). -export interface ConversationRequest { +export interface ConversationRequest extends AiRequestContext { message: string; - envProducts: Record<string, string[]>; - region: string; - tier: string; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/models/requests.ts` around lines 47 - 61, Extract the three shared properties (envProducts, region, tier) into a new base interface (e.g., SharedEnvContext or ConversationBase) and have both CaseClassificationRequest and ConversationRequest extend that base while keeping their distinct first fields (chatHistory and message respectively); update any imports/usages of CaseClassificationRequest or ConversationRequest to keep types compatible. Ensure the new base interface name and the extending declarations replace the duplicated properties in the declarations of ConversationRequest and CaseClassificationRequest.apps/customer-portal/webapp/src/pages/DescribeIssuePage.tsx (1)
63-66:useMemo/useCallbackare unnecessary with the React Compiler enabled.
envProducts(line 63) andhandleSubmit(line 81) are new additions that manually memoize. The project already runsbabel-plugin-react-compilerwhich handles these optimizations automatically.Based on learnings: the customer-portal webapp has
babel-plugin-react-compiler v1.0.0enabled, making manualuseCallback/useMemounnecessary.Also applies to: 81-109
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/pages/DescribeIssuePage.tsx` around lines 63 - 66, The memoization calls are unnecessary with babel-plugin-react-compiler enabled; remove useMemo around envProducts (currently created with buildEnvProducts) and remove useCallback wrapping handleSubmit so both are plain constants/functions; update the code to compute envProducts directly (const envProducts = buildEnvProducts(productsByDeploymentId, projectDeployments)) and define handleSubmit as a regular function/arrow function (e.g., const handleSubmit = (values) => { ... }) instead of using useMemo/useCallback, and delete their import/usages of React hooks if they become unused.apps/customer-portal/webapp/src/api/usePostConversations.ts (2)
49-59: Auth guard should precede debug logging.The debug log at lines 49–55 records
projectId,envProducts,region, andtierbefore the auth check at line 57. Move the guard first to avoid logging request details on behalf of unauthenticated callers.♻️ Proposed refactor
const { projectId, message, envProducts, region, tier } = params; + if (!isSignedIn || isAuthLoading) { + throw new Error("User must be signed in to send messages"); + } + logger.debug("[usePostConversations] Request:", { projectId, messageLength: message?.length, envProducts: Object.keys(envProducts || {}), region, tier, }); - if (!isSignedIn || isAuthLoading) { - throw new Error("User must be signed in to send messages"); - } - const baseUrl = window.config?.CUSTOMER_PORTAL_BACKEND_BASE_URL;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/api/usePostConversations.ts` around lines 49 - 59, The debug logging in usePostConversations.ts happens before the authentication guard; move the auth check (the if (!isSignedIn || isAuthLoading) throw new Error(...)) so it executes before the logger.debug call to avoid emitting request details for unauthenticated callers; update the function usePostConversations (or the surrounding scope) to perform the isSignedIn/isAuthLoading guard first and then run logger.debug with projectId, envProducts, region, tier, etc.
51-51: Unnecessary optional chain on non-optionalmessage.
ConversationRequest.messageis typed asstring(required), somessage?.lengthcan be simplified tomessage.length.♻️ Proposed fix
- messageLength: message?.length, + messageLength: message.length,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/api/usePostConversations.ts` at line 51, The code uses an unnecessary optional chain when computing messageLength: change the object property assignment from message?.length to message.length since ConversationRequest.message is non-optional; update the code in usePostConversations.ts where messageLength is set (reference symbol: message and property name messageLength) to use message.length to remove the redundant ?. operator.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/customer-portal/webapp/src/components/support/novera-ai-assistant/novera-chat-page/ChatMessageBubble.tsx`:
- Around line 79-90: The custom anchor renderer in ChatMessageBubble.tsx (the
inline a: ({ href, children }) => (...) renderer and the similar renderer at the
later occurrence) must explicitly sanitize and/or validate href to block
javascript: (and other unsafe) schemes; import DOMPurify and either (preferred)
add a urlTransform prop to the ReactMarkdown usage that runs
DOMPurify/validation on each URL and returns a safe URL or null, or sanitize
inside the renderer by computing a safeHref = DOMPurify.sanitize(href) and/or
validating the URL protocol (reject javascript:, data:, etc.), then use safeHref
for the anchor’s href and fallback to rendering plain text (no anchor) when
unsafe.
In `@apps/customer-portal/webapp/src/pages/__tests__/NoveraChatPage.test.tsx`:
- Around line 211-216: Update the two failing tests that still assert the old
banner text: in ChatInput.test.tsx (the test around the assertion at or near the
block referencing the escalation banner, e.g., where it checks for "Need more
help?") and in EscalationBanner.test.tsx (the test that asserts the banner
copy), replace the expected string "Need more help?" with the new text "Thank
you for describing the issue" so the assertions match the updated
EscalationBanner component; run the tests to verify the updated expectations
pass.
In `@apps/customer-portal/webapp/src/pages/DescribeIssuePage.tsx`:
- Around line 40-42: REGION and TIER are hardcoded in DescribeIssuePage.tsx and
duplicated in NoveraChatPage.tsx (classifyCase call), which prevents
multi-region/tier support; extract these literals into shared constants (e.g.,
export const REGION and TIER from a new or existing shared config/constants
module) and update both DescribeIssuePage.tsx and NoveraChatPage.tsx to import
and use those constants (later replace with dynamic values from
projectDetails.subscription.supportTier when multi-region support is added).
- Around line 57-66: The issue: envProducts can be an empty object if
productsByDeploymentId hasn't finished loading because isLoading from
useAllDeploymentProducts is ignored; update the submit guard to prevent
submission until products load and ensure envProducts is derived only after
loading. Concretely, use the isLoading (and/or isFetching) return value from
useAllDeploymentProducts alongside useGetProjectDeployments, pass those loading
flags into the memo that calls buildEnvProducts (or short-circuit to
null/undefined while loading), and add that combined loading flag into
isSubmitDisabled (the same variable used when handling the submit path that
constructs the conversation API payload). Locate the
hooks/useAllDeploymentProducts and the envProducts/useMemo + buildEnvProducts
usage and modify isSubmitDisabled to include the products loading state so the
first API call never runs with empty product context.
In `@apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx`:
- Around line 91-108: The code creates an empty user message bubble when
conversationResponse exists but initialUserMessage is undefined/empty; update
the array construction around botWelcome, the user message object (id "2") and
the bot message so that the user message object is only included when
initialUserMessage?.trim() is truthy (or build the array and filter out entries
with empty text) — adjust the logic near conversationResponse,
initialUserMessage, botWelcome and the created objects to conditionally include
or remove the user message to avoid inserting an empty bubble.
---
Nitpick comments:
In `@apps/customer-portal/webapp/src/api/usePostConversations.ts`:
- Around line 49-59: The debug logging in usePostConversations.ts happens before
the authentication guard; move the auth check (the if (!isSignedIn ||
isAuthLoading) throw new Error(...)) so it executes before the logger.debug call
to avoid emitting request details for unauthenticated callers; update the
function usePostConversations (or the surrounding scope) to perform the
isSignedIn/isAuthLoading guard first and then run logger.debug with projectId,
envProducts, region, tier, etc.
- Line 51: The code uses an unnecessary optional chain when computing
messageLength: change the object property assignment from message?.length to
message.length since ConversationRequest.message is non-optional; update the
code in usePostConversations.ts where messageLength is set (reference symbol:
message and property name messageLength) to use message.length to remove the
redundant ?. operator.
In
`@apps/customer-portal/webapp/src/components/support/novera-ai-assistant/novera-chat-page/ChatMessageBubble.tsx`:
- Around line 36-38: Replace the verbose indexed access type
React.ComponentProps<typeof ReactMarkdown>["components"] with the named export
Components from ReactMarkdown: locate the markdownComponents declaration and
change its type to ReactMarkdown.Components (or import { Components } from
"react-markdown" and use Components) so the variable markdownComponents uses the
named Components type for clarity and maintainability.
- Around line 36-91: markdownComponents currently lacks renderers for fenced
code blocks; update the markdownComponents object (used with ReactMarkdown) to
include handlers for code and pre that render styled, syntax-highlighted blocks
(e.g., use prismjs highlighting or a project-standard SyntaxHighlighter) and
ensure language classNames from node.properties.className are parsed to pass the
language to the highlighter; implement the code renderer to render inline code
as a styled <code> and block code inside a styled <pre> container with proper
aria and copy/line-wrap behavior consistent with the app's UI.
In `@apps/customer-portal/webapp/src/models/requests.ts`:
- Around line 47-61: Extract the three shared properties (envProducts, region,
tier) into a new base interface (e.g., SharedEnvContext or ConversationBase) and
have both CaseClassificationRequest and ConversationRequest extend that base
while keeping their distinct first fields (chatHistory and message
respectively); update any imports/usages of CaseClassificationRequest or
ConversationRequest to keep types compatible. Ensure the new base interface name
and the extending declarations replace the duplicated properties in the
declarations of ConversationRequest and CaseClassificationRequest.
In `@apps/customer-portal/webapp/src/models/responses.ts`:
- Around line 645-658: The ConversationResponse interface currently types
intent, slotState, actions, and resolved as unknown; update these fields to
stronger, backend-aligned types (or at minimum use Record<string, unknown> |
null or specific unions) so consumers can safely access properties—specifically
change actions to a nullable object type (e.g., Record<string, unknown> | null)
to match the non-null checks in NoveraChatPage.tsx and replace intent,
slotState, and resolved with concrete shapes or nullable records once the
backend schema is stabilized.
In `@apps/customer-portal/webapp/src/pages/DescribeIssuePage.tsx`:
- Around line 63-66: The memoization calls are unnecessary with
babel-plugin-react-compiler enabled; remove useMemo around envProducts
(currently created with buildEnvProducts) and remove useCallback wrapping
handleSubmit so both are plain constants/functions; update the code to compute
envProducts directly (const envProducts =
buildEnvProducts(productsByDeploymentId, projectDeployments)) and define
handleSubmit as a regular function/arrow function (e.g., const handleSubmit =
(values) => { ... }) instead of using useMemo/useCallback, and delete their
import/usages of React hooks if they become unused.
In `@apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx`:
- Around line 55-60: Replace the inline partial type used for location.state
with the canonical ConversationResponse type: import ConversationResponse from
the module that exports it (the same module where responses.ts defines it) and
change the cast on navState to use conversationResponse?: ConversationResponse
(keeping initialUserMessage?: string) so the component (NoveraChatPage /
navState, initialUserMessage, conversationResponse) stays in sync when
ConversationResponse evolves.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx (1)
55-58: Consider a shared type for the navigation state contract.
navStateis cast withashere and the producing side inDescribeIssuePagebuilds the same shape ad-hoc (Line 93-97 there). A shared interface (e.g.,ChatNavState) would keep both sides in sync and catch shape drift at compile time.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx` around lines 55 - 58, Create a shared interface (e.g., ChatNavState) describing { initialUserMessage?: string; conversationResponse?: { message: string; actions: unknown } } and export it from a common types module; then replace the ad-hoc cast in NoveraChatPage (navState) and the object construction in DescribeIssuePage with that shared ChatNavState type (use useLocation<ChatNavState | null>() or annotate the constructed object) so both producer (DescribeIssuePage) and consumer (navState in NoveraChatPage) use the same compile-time contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/customer-portal/webapp/src/pages/DescribeIssuePage.tsx`:
- Around line 81-101: Add an isSubmitting guard to handleSubmit to avoid
double-submission: create a local state (e.g., isSubmitting + setIsSubmitting),
return early if isSubmitting is true at the top of handleSubmit, set
isSubmitting to true before the async work and reset it to false in a finally
block so it always clears after success or error; update handleSubmit's
useCallback dependency array to include isSubmitting (and setIsSubmitting if
used) so the guard is effective; keep the existing isSubmitDisabled/Editor
disabled logic but rely on the new isSubmitting guard to prevent duplicate
keyboard-triggered submissions from onSubmitKeyDown.
---
Duplicate comments:
In `@apps/customer-portal/webapp/src/pages/DescribeIssuePage.tsx`:
- Around line 57-66: The submit path can run when deployment products are still
loading, producing an empty envProducts; update the component to read and use
isLoading from useAllDeploymentProducts (destructure isLoading from the hook
that returns productsByDeploymentId), and incorporate that flag into the
isSubmitDisabled logic so submissions are blocked while isLoading is true;
ensure envProducts (created via buildEnvProducts(productsByDeploymentId,
projectDeployments)) is only used when isLoading is false or non-empty to
prevent firing the conversation API with empty product context.
- Around line 40-42: Extract the duplicated literals REGION and TIER into a
shared constants module (e.g., export const REGION and TIER from a new shared
file) and replace the hardcoded declarations in DescribeIssuePage (remove const
REGION/TIER) and in NoveraChatPage where classifyCase is called so both files
import { REGION, TIER } from the new module; ensure exports use the same symbol
names so existing references (REGION, TIER and the classifyCase call in
NoveraChatPage) compile without further changes.
In `@apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx`:
- Around line 90-106: The current return always inserts a user message object
even when initialUserMessage is undefined/blank, causing an empty user bubble;
change the logic around conversationResponse?.message so you only include the
user message object when initialUserMessage?.trim() is non-empty — e.g., build
the messages array conditionally (push or spread) so that if
initialUserMessage?.trim() is falsy you return just the bot message object with
text: conversationResponse.message and showCreateCaseAction:
conversationResponse.actions != null; reference the existing symbols
conversationResponse?.message, initialUserMessage, and showCreateCaseAction when
making the conditional inclusion.
---
Nitpick comments:
In `@apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx`:
- Around line 55-58: Create a shared interface (e.g., ChatNavState) describing {
initialUserMessage?: string; conversationResponse?: { message: string; actions:
unknown } } and export it from a common types module; then replace the ad-hoc
cast in NoveraChatPage (navState) and the object construction in
DescribeIssuePage with that shared ChatNavState type (use
useLocation<ChatNavState | null>() or annotate the constructed object) so both
producer (DescribeIssuePage) and consumer (navState in NoveraChatPage) use the
same compile-time contract.
Introduce conversation defaults, shared types, and safety fixes for the chat flow. - Add DEFAULT_CONVERSATION_REGION and DEFAULT_CONVERSATION_TIER constants. - Add ChatNavState model for navigation state between DescribeIssuePage and NoveraChatPage. - Introduce SharedEnvContext and update ConversationRequest / CaseClassificationRequest to extend it. - Move sign-in/auth check earlier and tighten logging in usePostConversations. - Sanitize markdown link rendering in ChatMessageBubble to only allow safe protocols (http/https), falling back to non-link text for unsafe hrefs. - Update DescribeIssuePage to use defaults, prevent duplicate submissions with a ref, include product-loading in submit-disable logic, and pass ChatNavState when navigating to chat. - Update NoveraChatPage to consume ChatNavState, properly assemble initial messages, and use the default region/tier for classification. - Update unit tests mocks and expectations to match component/icon changes. These changes centralize environment defaults, improve type reuse, harden link rendering, and stabilize the describe->chat navigation/submit behavior.
2ca4510
into
wso2-open-operations:customer-portal-milestone-1
Summary by CodeRabbit
New Features
Tests
Chores