Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
Caution Review failedThe pull request is closed. WalkthroughThis update introduces a local database-backed caching layer for email threads, enabling label-based filtering and improved synchronization between server and client. Major changes include new hooks and schemas for label management, modifications to thread retrieval and synchronization logic, and updates to API procedures to utilize the local cache. Several files are refactored for type safety and consistency. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant TRPC
participant AgentRpcDO
participant ZeroAgent
participant SQLite
participant KV
participant MailDriver
Client->>TRPC: listThreads({ labelIds, ... })
TRPC->>AgentRpcDO: getThreadsFromDB({ labelIds, ... })
AgentRpcDO->>ZeroAgent: getThreadsFromDB({ labelIds, ... })
ZeroAgent->>SQLite: Query threads table (with label/folder filters)
SQLite-->>ZeroAgent: Thread metadata rows
ZeroAgent-->>AgentRpcDO: Thread IDs and pagination token
AgentRpcDO-->>TRPC: Thread list
TRPC-->>Client: Thread list
Client->>TRPC: get(threadId)
TRPC->>AgentRpcDO: getThreadFromDB(threadId)
AgentRpcDO->>ZeroAgent: getThreadFromDB(threadId)
ZeroAgent->>SQLite: Query threads table for threadId
alt Thread found
ZeroAgent->>KV: Get messages for threadId
KV-->>ZeroAgent: Messages
ZeroAgent-->>AgentRpcDO: Thread data
else Thread not found
ZeroAgent->>MailDriver: Fetch thread
MailDriver-->>ZeroAgent: Thread data
ZeroAgent->>KV: Store messages
ZeroAgent->>SQLite: Insert thread metadata
ZeroAgent-->>AgentRpcDO: Thread data
end
AgentRpcDO-->>TRPC: Thread data
TRPC-->>Client: Thread data
Possibly related PRs
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (15)
✨ 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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
a8b245f to
c86fefa
Compare
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (2)
apps/mail/components/mail/mail.tsx (1)
586-590: Clarify the purpose of commenting out the CategorySelect component.The CategorySelect component has been commented out instead of being properly removed or conditionally rendered. This suggests the change might be temporary or incomplete.
If this is related to the new label filtering mechanism mentioned in the PR summary, consider:
- Adding a feature flag or conditional logic instead of commenting out
- Completely removing the code if the feature is permanently deprecated
- Adding a TODO comment explaining when/why this should be restored
apps/server/src/routes/chat.ts (1)
827-885: Well-implemented thread synchronization with proper error handlingThe
syncThreadmethod properly handles data persistence to both R2 and the database, with appropriate error handling and conditional broadcasting.Consider adding more context to the error message:
} catch (error) { - console.error(`Failed to sync thread ${threadId}:`, error); + console.error(`Failed to sync thread ${threadId} for connection ${this.name}:`, error); throw error; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
apps/mail/components/mail/mail-list.tsx(1 hunks)apps/mail/components/mail/mail.tsx(2 hunks)apps/mail/components/party.tsx(2 hunks)apps/mail/components/ui/recursive-folder.tsx(2 hunks)apps/mail/hooks/use-labels-search.ts(1 hunks)apps/mail/hooks/use-threads.ts(2 hunks)apps/server/src/lib/driver/google.ts(1 hunks)apps/server/src/lib/driver/types.ts(1 hunks)apps/server/src/lib/server-utils.ts(2 hunks)apps/server/src/main.ts(4 hunks)apps/server/src/pipelines.ts(2 hunks)apps/server/src/routes/chat.ts(10 hunks)apps/server/src/trpc/routes/mail.ts(7 hunks)apps/server/src/types.ts(2 hunks)apps/server/wrangler.jsonc(3 hunks)
🧰 Additional context used
🧠 Learnings (5)
apps/mail/hooks/use-threads.ts (1)
Learnt from: retrogtx
PR: Mail-0/Zero#1328
File: apps/mail/lib/hotkeys/mail-list-hotkeys.tsx:202-209
Timestamp: 2025-06-18T17:26:50.918Z
Learning: In apps/mail/lib/hotkeys/mail-list-hotkeys.tsx, the switchCategoryByIndex function using hardcoded indices for category hotkeys does not break when users reorder categories, contrary to the theoretical index-shifting issue. The actual implementation has constraints or mechanisms that prevent hotkey targeting issues.
apps/mail/components/mail/mail-list.tsx (3)
Learnt from: retrogtx
PR: Mail-0/Zero#1328
File: apps/mail/lib/hotkeys/mail-list-hotkeys.tsx:202-209
Timestamp: 2025-06-18T17:26:50.918Z
Learning: In apps/mail/lib/hotkeys/mail-list-hotkeys.tsx, the switchCategoryByIndex function using hardcoded indices for category hotkeys does not break when users reorder categories, contrary to the theoretical index-shifting issue. The actual implementation has constraints or mechanisms that prevent hotkey targeting issues.
Learnt from: danteissaias
PR: Mail-0/Zero#902
File: apps/mail/components/connection/add.tsx:77-77
Timestamp: 2025-05-07T16:55:46.513Z
Learning: For the "Upgrade" link in AddConnectionDialog, using a proper <button> element instead of a <span> with onClick is recognized as an accessibility improvement but was deferred as out of scope in PR #902 (CSS variables PR).
Learnt from: snehendu098
PR: Mail-0/Zero#1323
File: apps/mail/lib/themes/theme-utils.ts:318-318
Timestamp: 2025-06-24T06:22:58.753Z
Learning: In the Mail-0/Zero theme system (apps/mail/lib/themes/theme-utils.ts), when color themes are being applied, all color values come in HSL format, so there's no need for additional format validation when converting colors with hslToHex().
apps/mail/components/ui/recursive-folder.tsx (1)
Learnt from: retrogtx
PR: Mail-0/Zero#1328
File: apps/mail/lib/hotkeys/mail-list-hotkeys.tsx:202-209
Timestamp: 2025-06-18T17:26:50.918Z
Learning: In apps/mail/lib/hotkeys/mail-list-hotkeys.tsx, the switchCategoryByIndex function using hardcoded indices for category hotkeys does not break when users reorder categories, contrary to the theoretical index-shifting issue. The actual implementation has constraints or mechanisms that prevent hotkey targeting issues.
apps/mail/components/mail/mail.tsx (1)
Learnt from: retrogtx
PR: Mail-0/Zero#1328
File: apps/mail/lib/hotkeys/mail-list-hotkeys.tsx:202-209
Timestamp: 2025-06-18T17:26:50.918Z
Learning: In apps/mail/lib/hotkeys/mail-list-hotkeys.tsx, the switchCategoryByIndex function using hardcoded indices for category hotkeys does not break when users reorder categories, contrary to the theoretical index-shifting issue. The actual implementation has constraints or mechanisms that prevent hotkey targeting issues.
apps/mail/components/party.tsx (1)
Learnt from: retrogtx
PR: Mail-0/Zero#1328
File: apps/mail/lib/hotkeys/mail-list-hotkeys.tsx:202-209
Timestamp: 2025-06-18T17:26:50.918Z
Learning: In apps/mail/lib/hotkeys/mail-list-hotkeys.tsx, the switchCategoryByIndex function using hardcoded indices for category hotkeys does not break when users reorder categories, contrary to the theoretical index-shifting issue. The actual implementation has constraints or mechanisms that prevent hotkey targeting issues.
🧬 Code Graph Analysis (7)
apps/server/src/pipelines.ts (1)
apps/server/src/lib/server-utils.ts (1)
notifyUser(59-87)
apps/mail/components/mail/mail-list.tsx (1)
apps/mail/lib/utils.ts (1)
cn(51-51)
apps/server/src/types.ts (1)
apps/mail/types/index.ts (1)
ParsedMessage(61-87)
apps/server/src/lib/driver/types.ts (2)
apps/server/src/types.ts (2)
ParsedMessage(144-144)ParsedMessageSchema(105-142)apps/mail/types/index.ts (1)
ParsedMessage(61-87)
apps/server/src/lib/server-utils.ts (1)
apps/server/src/routes/chat.ts (1)
OutgoingMessage(100-128)
apps/server/src/trpc/routes/mail.ts (2)
apps/server/src/lib/driver/types.ts (1)
IGetThreadResponseSchema(14-20)apps/server/src/lib/server-utils.ts (1)
getZeroAgent(14-19)
apps/server/src/routes/chat.ts (3)
apps/server/src/lib/driver/types.ts (2)
IGetThreadResponse(6-12)MailManager(50-105)apps/server/src/types.ts (1)
ParsedMessage(144-144)apps/mail/types/index.ts (1)
ParsedMessage(61-87)
🔇 Additional comments (20)
apps/server/wrangler.jsonc (1)
22-27: LGTM! Well-structured R2 bucket configuration.The R2 bucket configurations are properly structured across all environments with appropriate naming conventions. The binding name
THREADS_BUCKETis descriptive and the bucket names follow a consistent pattern.Also applies to: 187-192, 292-297
apps/server/src/main.ts (1)
692-692: LGTM! Improved logging for better observability.The addition of
[SCHEDULED]prefixes to console logs enhances debugging and monitoring capabilities for scheduled subscription checks. The logging is consistent and well-structured.Also applies to: 694-694, 708-710, 719-721, 730-730
apps/mail/hooks/use-threads.ts (1)
6-6: LGTM! Proper integration of label-based filtering.The integration of the
useSearchLabelshook is implemented correctly and enables label-based thread filtering as intended. The destructuring and usage pattern follows established conventions.Note: This integration depends on fixing the null handling issue in the
useSearchLabelshook to ensurelabelsis always a defined array.Also applies to: 21-21, 28-28
apps/server/src/pipelines.ts (1)
22-22: LGTM! Proper integration of user notification after thread retrieval.The
notifyUsercall is correctly placed after thread retrieval and properly passes the required parameters (connectionId,result,threadId). This enables real-time broadcasting of thread updates to connected clients, supporting the new caching and synchronization features.Also applies to: 400-404
apps/mail/components/mail/mail-list.tsx (1)
272-275: LGTM! Good UX improvement for read/unread thread distinction.The conditional opacity application correctly implements the visual dimming of read threads while keeping unread threads at full opacity, providing clear visual distinction for users.
apps/server/src/lib/driver/google.ts (1)
356-360: LGTM! Excellent defensive programming for header consistency.The nullish coalescing ensures that header
nameandvalueproperties are always strings, preventing potential null/undefined issues in downstream processing and supporting the typed schema validation mentioned in the broader system changes.apps/mail/components/mail/mail.tsx (1)
599-599: Height adjustment correctly compensates for removed CategorySelect.The height calculation change from
calc(100dvh - 9.8rem)tocalc(100dvh - 7rem)logically compensates for the commented-out CategorySelect component, making more space available for the MailList component.apps/server/src/types.ts (3)
2-2: LGTM!The Zod import is correctly added to support the new schema-based validation approach.
105-142: Excellent migration to Zod schema for runtime validation.The comprehensive
ParsedMessageSchemaproperly replaces the TypeScript interface with runtime validation capabilities. The schema correctly handles:
- All required and optional fields from the original interface
- Nested object validation for tags, sender, and recipients
- Proper nullable handling for cc/bcc arrays
- Well-structured attachment schema with proper typing
This change enhances type safety at runtime and provides better error handling for API boundaries.
144-144: Proper use of Zod type inference.Using
z.infer<typeof ParsedMessageSchema>correctly derives the TypeScript type from the schema, ensuring the type and validation rules stay synchronized.apps/mail/components/ui/recursive-folder.tsx (3)
6-6: LGTM!The import of
useSearchLabelshook supports the improved ID-based label filtering mechanism.
22-23: Improved filtering mechanism with ID-based approach.The migration from string-based search to ID-based filtering using
useSearchLabelsis a significant improvement that provides:
- More reliable label matching
- Better performance
- Cleaner logic with array inclusion checks
28-37: Well-implemented label toggle logic.The array-based label filtering logic is clean and efficient:
- Properly toggles label IDs in the array
- Uses appropriate array methods for state management
- Correctly updated dependency array for the callback
The implementation follows React best practices and is more maintainable than the previous string manipulation approach.
apps/server/src/lib/driver/types.ts (3)
2-4: LGTM!The imports correctly support the new schema-based validation approach, maintaining consistency with the broader codebase migration to Zod.
8-8: Improved TypeScript syntax for optional property.Changing from
latest: ParsedMessage | undefinedtolatest?: ParsedMessageis cleaner and more idiomatic TypeScript syntax for optional properties.
14-20: Well-structured Zod schema for thread response validation.The
IGetThreadResponseSchemacorrectly validates the thread response structure:
- Properly uses
ParsedMessageSchemafor message validation- Correctly handles the optional
latestproperty- Maintains consistency with the interface definition
- Provides runtime validation for API responses
apps/mail/components/party.tsx (2)
9-24: Excellent addition of message type enums.The
IncomingMessageTypeandOutgoingMessageTypeenums provide:
- Better type safety for WebSocket message handling
- Centralized message type definitions
- Clear and consistent naming conventions
- Improved code maintainability
49-52: Streamlined message handling improves efficiency.The simplified message handling logic provides:
- Direct cache updates instead of query invalidations
- More efficient real-time thread updates
- Cleaner code with focused responsibility
- Proper use of the new message type enum
The direct
setQueryDataapproach is more performant than invalidating and refetching queries.apps/server/src/trpc/routes/mail.ts (1)
23-23: Good addition of output schema for type safetyAdding the output schema ensures type-safe responses from the get procedure.
apps/server/src/routes/chat.ts (1)
519-546: Good integration of database-backed methods in WebSocket handlersThe WebSocket message handling properly integrates with the new caching layer, using
getThreadsFromDBandgetThreadFromDBmethods.
apps/server/src/trpc/routes/mail.ts
Outdated
| await agent.sendDraft(draftId, mail as any); | ||
| } else { | ||
| await agent.create(input); | ||
| await agent.create(input as any); |
There was a problem hiding this comment.
Avoid casting to any - fix type definitions instead
Casting to any defeats TypeScript's type safety and can hide potential runtime errors. The underlying type mismatch should be resolved.
Instead of casting, ensure the types match properly:
- await agent.sendDraft(draftId, mail as any);
+ await agent.sendDraft(draftId, mail);
} else {
- await agent.create(input as any);
+ await agent.create(input);If there's a type mismatch, update the method signatures or transform the data to match the expected types.
📝 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.
| await agent.sendDraft(draftId, mail as any); | |
| } else { | |
| await agent.create(input); | |
| await agent.create(input as any); | |
| await agent.sendDraft(draftId, mail); | |
| } else { | |
| await agent.create(input); |
🤖 Prompt for AI Agents
In apps/server/src/trpc/routes/mail.ts around lines 328 to 330, the code casts
variables to 'any' which bypasses TypeScript's type checking. To fix this,
review the expected parameter types for the sendDraft and create methods and
update the input data or method signatures so the types align correctly without
using 'any'. This may involve adjusting the input object's type or transforming
it to match the method requirements.
| async getThreadsFromDB(params: { | ||
| labelIds?: string[]; | ||
| folder?: string; | ||
| q?: string; | ||
| max?: number; | ||
| cursor?: string; | ||
| }) { | ||
| const { labelIds = [], folder, q, max = 50, cursor } = params; | ||
|
|
||
| try { | ||
| // Build WHERE conditions | ||
| const whereConditions: string[] = []; | ||
|
|
||
| // Add folder condition (maps to specific label) | ||
| if (folder) { | ||
| const folderLabel = folder.toUpperCase(); | ||
| whereConditions.push(`EXISTS ( | ||
| SELECT 1 FROM json_each(latest_label_ids) WHERE value = '${folderLabel}' | ||
| )`); | ||
| } | ||
|
|
||
| // Add label conditions (OR logic for multiple labels) | ||
| if (labelIds.length > 0) { | ||
| if (labelIds.length === 1) { | ||
| whereConditions.push(`EXISTS ( | ||
| SELECT 1 FROM json_each(latest_label_ids) WHERE value = '${labelIds[0]}' | ||
| )`); | ||
| } else { | ||
| // Multiple labels with OR logic | ||
| const multiLabelCondition = labelIds | ||
| .map( | ||
| (labelId) => | ||
| `EXISTS (SELECT 1 FROM json_each(latest_label_ids) WHERE value = '${labelId}')`, | ||
| ) | ||
| .join(' OR '); | ||
| whereConditions.push(`(${multiLabelCondition})`); | ||
| } | ||
| } | ||
|
|
||
| // // Add search query condition | ||
| // if (q) { | ||
| // const searchTerm = q.replace(/'/g, "''"); // Escape single quotes | ||
| // whereConditions.push(`( | ||
| // latest_subject LIKE '%${searchTerm}%' OR | ||
| // latest_sender LIKE '%${searchTerm}%' OR | ||
| // messages LIKE '%${searchTerm}%' | ||
| // )`); | ||
| // } | ||
|
|
||
| // Add cursor condition | ||
| if (cursor) { | ||
| whereConditions.push(`latest_received_on < '${cursor}'`); | ||
| } | ||
|
|
||
| // Execute query based on conditions | ||
| let result; | ||
|
|
||
| if (whereConditions.length === 0) { | ||
| // No conditions | ||
| result = await this.sql` | ||
| SELECT id, latest_received_on | ||
| FROM threads | ||
| ORDER BY latest_received_on DESC | ||
| LIMIT ${max} | ||
| `; | ||
| } else if (whereConditions.length === 1) { | ||
| // Single condition | ||
| const condition = whereConditions[0]; | ||
| if (condition.includes('latest_received_on <')) { | ||
| const cursorValue = cursor!; | ||
| result = await this.sql` | ||
| SELECT id, latest_received_on | ||
| FROM threads | ||
| WHERE latest_received_on < ${cursorValue} | ||
| ORDER BY latest_received_on DESC | ||
| LIMIT ${max} | ||
| `; | ||
| } else if (folder) { | ||
| // Folder condition | ||
| const folderLabel = folder.toUpperCase(); | ||
| result = await this.sql` | ||
| SELECT id, latest_received_on | ||
| FROM threads | ||
| WHERE EXISTS ( | ||
| SELECT 1 FROM json_each(latest_label_ids) WHERE value = ${folderLabel} | ||
| ) | ||
| ORDER BY latest_received_on DESC | ||
| LIMIT ${max} | ||
| `; | ||
| } else { | ||
| // Single label condition | ||
| const labelId = labelIds[0]; | ||
| result = await this.sql` | ||
| SELECT id, latest_received_on | ||
| FROM threads | ||
| WHERE EXISTS ( | ||
| SELECT 1 FROM json_each(latest_label_ids) WHERE value = ${labelId} | ||
| ) | ||
| ORDER BY latest_received_on DESC | ||
| LIMIT ${max} | ||
| `; | ||
| } | ||
| } else { | ||
| // Multiple conditions - handle combinations | ||
| if (folder && labelIds.length === 0 && cursor) { | ||
| // Folder + cursor | ||
| const folderLabel = folder.toUpperCase(); | ||
| result = await this.sql` | ||
| SELECT id, latest_received_on | ||
| FROM threads | ||
| WHERE EXISTS ( | ||
| SELECT 1 FROM json_each(latest_label_ids) WHERE value = ${folderLabel} | ||
| ) AND latest_received_on < ${cursor} | ||
| ORDER BY latest_received_on DESC | ||
| LIMIT ${max} | ||
| `; | ||
| } else if (labelIds.length === 1 && cursor && !folder) { | ||
| // Single label + cursor | ||
| const labelId = labelIds[0]; | ||
| result = await this.sql` | ||
| SELECT id, latest_received_on | ||
| FROM threads | ||
| WHERE EXISTS ( | ||
| SELECT 1 FROM json_each(latest_label_ids) WHERE value = ${labelId} | ||
| ) AND latest_received_on < ${cursor} | ||
| ORDER BY latest_received_on DESC | ||
| LIMIT ${max} | ||
| `; | ||
| } else { | ||
| // For now, fallback to just cursor if complex combinations | ||
| const cursorValue = cursor || ''; | ||
| result = await this.sql` | ||
| SELECT id, latest_received_on | ||
| FROM threads | ||
| WHERE latest_received_on < ${cursorValue} | ||
| ORDER BY latest_received_on DESC | ||
| LIMIT ${max} | ||
| `; | ||
| } | ||
| } | ||
|
|
||
| const threads = result.map((row: any) => ({ | ||
| id: row.id, | ||
| historyId: null, | ||
| })); | ||
|
|
||
| // Use latest_received_on for pagination cursor | ||
| const nextPageToken = | ||
| threads.length === max && result.length > 0 | ||
| ? result[result.length - 1].latest_received_on | ||
| : null; | ||
|
|
||
| return { | ||
| threads, | ||
| nextPageToken, | ||
| }; | ||
| } catch (error) { | ||
| console.error('Failed to get threads from database:', error); | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
❌ Incorrect review comment
Potential SQL injection risk in query building
The getThreadsFromDB method builds SQL queries using string interpolation for label IDs (lines 964-984), which could lead to SQL injection if these values contain malicious input.
Use parameterized queries consistently throughout:
- whereConditions.push(`EXISTS (
- SELECT 1 FROM json_each(latest_label_ids) WHERE value = '${folderLabel}'
- )`);
+ // Use parameterized query instead
+ whereConditions.push(`EXISTS (
+ SELECT 1 FROM json_each(latest_label_ids) WHERE value = ?
+ )`);
+ // And bind the parameter when executingLet me verify if labelIds can contain user input:
🏁 Script executed:
#!/bin/bash
# Check where labelIds come from in the codebase
rg -A 5 "labelIds.*=" --type tsLength of output: 4302
Potential SQL injection risk in query building
The getThreadsFromDB method builds SQL queries using string interpolation for label IDs (lines 964-984), which could lead to SQL injection if these values contain malicious input.
Use parameterized queries consistently throughout:
- whereConditions.push(`EXISTS (
- SELECT 1 FROM json_each(latest_label_ids) WHERE value = '${folderLabel}'
- )`);
+ // Use parameterized query instead
+ whereConditions.push(`EXISTS (
+ SELECT 1 FROM json_each(latest_label_ids) WHERE value = ?
+ )`);
+ // And bind the parameter when executingLet me verify if labelIds can contain user input:
#!/bin/bash
# Check where labelIds come from in the codebase
rg -A 5 "labelIds.*=" --type ts🤖 Prompt for AI Agents
In apps/server/src/routes/chat.ts between lines 948 and 1108, the
getThreadsFromDB method constructs SQL queries by directly interpolating
labelIds and folder values into query strings, which risks SQL injection. To fix
this, replace all string interpolations for labelIds and folder with
parameterized query bindings using the SQL library's templating syntax to safely
pass these values as parameters instead of embedding them directly in the query
strings.
c86fefa to
66cb890
Compare

READ CAREFULLY THEN REMOVE
Remove bullet points that are not relevant.
PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI.
Description
Please provide a clear description of your changes.
Type of Change
Please delete options that are not relevant.
Areas Affected
Please check all that apply:
Testing Done
Describe the tests you've done:
Security Considerations
For changes involving data or authentication:
Checklist
Additional Notes
Add any other context about the pull request here.
Screenshots/Recordings
Add screenshots or recordings here if applicable.
By submitting this pull request, I confirm that my contribution is made under the terms of the project's license.
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Developer Experience