feat: Reimplement support for non-root app predictions#6745
Conversation
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughAdds a Lawnchair prediction system with selectable modes, persisted usage and dismissal tracking, ranking and event resolution, predictor lifecycle wiring, launcher refresh behavior, and new preferences and navigation entries for managing predictions. ChangesApp Predictions Feature
Sequence Diagram(s)sequenceDiagram
participant StatsLogCompatManager
participant LawnchairAppPredictor
participant PredictionEventResolver
participant LawnchairPredictionEngine
participant LauncherModel
StatsLogCompatManager->>LawnchairAppPredictor: consume(event, atomInfo)
LawnchairAppPredictor->>PredictionEventResolver: resolve(atomInfo)
LawnchairAppPredictor->>LawnchairPredictionEngine: getFallbackRanked()
LawnchairAppPredictor->>LawnchairPredictionEngine: compileAppTargets()
LawnchairAppPredictor->>LawnchairPredictionEngine: compileWidgetTargets()
LawnchairAppPredictor->>LauncherModel: enqueue PredictionUpdateTask / WidgetsPredictionUpdateTask
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches✨ Simplify code
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 |
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lawnchair/src/app/lawnchair/predictions/LawnchairAppPredictor.kt`:
- Around line 456-470: recordLaunchEvent() currently only matches locations like
"hotseat"/"workspace"/"folder" and "predictions", so launches resolved as
"predictions/hotseat" (emitted by resolveLocation()) are ignored; update the
matching logic in recordLaunchEvent() to include the "predictions/hotseat" case
(e.g., check resolvedEvent.location.startsWith("predictions/hotseat") or use
contains("hotseat") alongside the existing checks) so that
hotseatStore.record(key) is called for those events; ensure you reference the
resolvedEvent.location branching and the hotseatStore.record(key) branch when
making the change.
- Around line 247-250: In shouldUseWeightedUsageStats(), replace the incorrect
runtime permission check using
context.checkSelfPermission(Manifest.permission.PACKAGE_USAGE_STATS) ==
PackageManager.PERMISSION_GRANTED with an AppOpsManager check: obtain
AppOpsManager via context.getSystemService(Context.APP_OPS_SERVICE) as
AppOpsManager and call checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS,
Process.myUid(), context.packageName) and return true only if it equals
AppOpsManager.MODE_ALLOWED; keep the existing
prefs2.lawnchairPredictorUseWeightedUsageStats.firstBlocking() guard as-is.
In `@lawnchair/src/app/lawnchair/predictions/PredictionMode.kt`:
- Around line 35-37: Replace the current runtime-permission check that uses
context.checkSelfPermission(Manifest.permission.PACKAGE_USAGE_STATS) and
PackageManager.PERMISSION_GRANTED with an AppOps-based check: obtain
AppOpsManager and call unsafeCheckOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS,
Process.myUid(), context.packageName) to determine if usage access is allowed
for the app (this will correctly reflect the Usage Access setting); keep the
existing presence check for AppPredictionManager via
context.getSystemService(AppPredictionManager::class.java), and if the AppOps
result indicates denied, ensure callers are directed to
Settings.ACTION_USAGE_ACCESS_SETTINGS so users can grant Usage Access.
In
`@lawnchair/src/app/lawnchair/ui/preferences/destinations/PredictionsPreferences.kt`:
- Around line 66-68: The current check that sets hasUsageStatsPermission in
PredictionsPreferences.kt uses context.checkSelfPermission(PACKAGE_USAGE_STATS)
which is wrong for app-op controlled usage access; replace that logic with an
AppOpsManager-based check: obtain AppOpsManager via context.getSystemService,
call appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS,
Binder.getCallingUid() or context.applicationInfo.uid, context.packageName) and
treat the result as allowed when equals AppOpsManager.MODE_ALLOWED (or
MODE_ALLOWED/allowed equivalent), then use that boolean for the weighted usage
stats toggle. Update any references to hasUsageStatsPermission so the toggle at
line ~102 reflects the AppOps result instead of checkSelfPermission.
🪄 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: CHILL
Plan: Pro
Run ID: 8062f6d6-46dc-4243-90c4-b7676a49f790
📒 Files selected for processing (24)
build.gradlegradle/libs.versions.tomllawnchair/res/values/config.xmllawnchair/res/values/strings.xmllawnchair/src/app/lawnchair/LawnchairLauncher.ktlawnchair/src/app/lawnchair/predictions/AppUsageStore.ktlawnchair/src/app/lawnchair/predictions/DismissedPredictionAppsStore.ktlawnchair/src/app/lawnchair/predictions/LawnchairAppPredictor.ktlawnchair/src/app/lawnchair/predictions/LawnchairModelDelegate.ktlawnchair/src/app/lawnchair/predictions/PredictionAppKey.ktlawnchair/src/app/lawnchair/predictions/PredictionMode.ktlawnchair/src/app/lawnchair/preferences2/PreferenceManager2.ktlawnchair/src/app/lawnchair/ui/preferences/components/controls/NavigationSwitchPreference.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/DismissedPredictionAppsPreferences.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/ExperimentalFeaturesPreferences.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/GeneralPreferences.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/PredictionsPreferences.ktlawnchair/src/app/lawnchair/ui/preferences/navigation/PreferenceNavigation.ktlawnchair/src/app/lawnchair/ui/preferences/navigation/PreferenceRoutes.ktquickstep/dagger/com/android/launcher3/dagger/AppModule.ktquickstep/src/com/android/launcher3/model/PredictionUpdateTask.javaquickstep/src/com/android/launcher3/model/QuickstepModelDelegate.javaquickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.javaquickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
💤 Files with no reviewable changes (1)
- lawnchair/src/app/lawnchair/ui/preferences/destinations/ExperimentalFeaturesPreferences.kt
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Co-authored-by: TheSuperDragon <70206496+SuperDragonXD@users.noreply.github.com>
Co-authored-by: SuperDragonXD <70206496+superdragonxd@users.noreply.github.com> Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
…ingExperiments/lawnchair into feature-16-dev_appprediction
Co-authored-by: SuperDragonXD <70206496+superdragonxd@users.noreply.github.com> Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Co-authored-by: SuperDragonXD <70206496+superdragonxd@users.noreply.github.com> Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Co-authored-by: SuperDragonXD <70206496+superdragonxd@users.noreply.github.com> Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
lawnchair/src/app/lawnchair/predictions/LawnchairAppPredictor.kt (1)
130-132: 🏗️ Heavy liftAvoid full uninstall-pruning on every event-driven update
updates()always callsprunePredictionStores(), which rechecks installed state across all stores on each launch/dismiss/layout mutation. In this hot path, that’s avoidable churn on MODEL_EXECUTOR. Consider pruning on package/profile change signals (or a throttled interval) instead.Also applies to: 236-241
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lawnchair/src/app/lawnchair/predictions/LawnchairAppPredictor.kt` around lines 130 - 132, The prunePredictionStores() call in the updates() method is being executed on every event-driven update (launch/dismiss/layout mutation), causing unnecessary churn on MODEL_EXECUTOR by rechecking installed state across all stores repeatedly. Remove the prunePredictionStores(pm) call from the updates() method and instead invoke it only on package/profile change signals or implement a throttled interval-based approach to prune stores less frequently. This prevents performance degradation in this hot code path while still maintaining the pruning functionality when it actually matters.lawnchair/src/app/lawnchair/predictions/LawnchairPredictionEngine.kt (1)
129-135: ⚡ Quick winMake
mergeRankedlinear-time with a seen setCurrent dedupe (
key !in this) is quadratic. This runs in the prediction update path, so use aLinkedHashSetto keep order with O(1) membership checks.Suggested refactor
- fun mergeRanked(vararg rankedLists: List<String>): List<String> = buildList { - rankedLists.forEach { ranked -> - ranked.forEach { key -> - if (key !in this) add(key) - } - } - } + fun mergeRanked(vararg rankedLists: List<String>): List<String> { + val seen = LinkedHashSet<String>() + rankedLists.forEach { ranked -> + ranked.forEach { key -> seen.add(key) } + } + return seen.toList() + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lawnchair/src/app/lawnchair/predictions/LawnchairPredictionEngine.kt` around lines 129 - 135, The mergeRanked function has a performance issue where checking membership with `key !in this` on the buildList result is O(n) for each key, resulting in quadratic overall time complexity. Fix this by introducing a LinkedHashSet to track seen keys separately before adding them to the result list, which provides O(1) membership checks while maintaining insertion order. Replace the current logic that checks `key !in this` with a check against the LinkedHashSet, add the key to both the LinkedHashSet and the result list when it's not seen, and return the buildList result as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@build.gradle`:
- Line 349: The capitalizedVariantName definition uses toUpperCase() without
specifying a locale, which can cause non-reproducible builds across different
system locales. Add Locale.ROOT as an argument to the toUpperCase() method call
in the substring expression to ensure locale-stable capitalization behavior that
works consistently regardless of the system's default locale.
In `@lawnchair/src/app/lawnchair/predictions/LawnchairPredictionEngine.kt`:
- Around line 218-221: In the parseStoreKey function, the line that assigns the
user variable currently uses the Elvis operator with Process.myUserHandle() as a
fallback when resolveUserToken returns null, which silently remaps unknown user
tokens to the current user. Instead, change this line to return null immediately
when resolveUserToken fails by replacing the Process.myUserHandle() fallback
with return null, ensuring invalid keys are skipped rather than misattributed to
the current profile.
In `@lawnchair/src/app/lawnchair/predictions/LawnchairPredictionStore.kt`:
- Around line 25-45: The cache mutable list is shared state accessed from
multiple execution contexts without synchronization, which can cause race
conditions and concurrent modification issues. Protect all access to the cache
variable by wrapping the operations in the add() method and all other
cache-accessing methods (mentioned at lines 51-55, 62-75, 79-94, 102-104) with
synchronization using either a synchronized block with a lock object or Kotlin's
mutex to ensure thread-safe read and write operations on the shared cache state.
In
`@lawnchair/src/app/lawnchair/ui/preferences/destinations/PredictionsPreferences.kt`:
- Around line 159-163: The dismissedPredictionAppsCount is initialized once with
a snapshot of dismissedAppsStore.getEntries().size and never updates when the
store changes, causing stale data in the UI. Replace the mutable state
initialization to observe dismissedAppsStore for changes and update the count
reactively. Use a LaunchedEffect or SideEffect block to listen for store updates
and synchronize dismissedPredictionAppsCount whenever
dismissedAppsStore.getEntries() changes, ensuring the subtitle always reflects
the current dismissed apps count.
---
Nitpick comments:
In `@lawnchair/src/app/lawnchair/predictions/LawnchairAppPredictor.kt`:
- Around line 130-132: The prunePredictionStores() call in the updates() method
is being executed on every event-driven update (launch/dismiss/layout mutation),
causing unnecessary churn on MODEL_EXECUTOR by rechecking installed state across
all stores repeatedly. Remove the prunePredictionStores(pm) call from the
updates() method and instead invoke it only on package/profile change signals or
implement a throttled interval-based approach to prune stores less frequently.
This prevents performance degradation in this hot code path while still
maintaining the pruning functionality when it actually matters.
In `@lawnchair/src/app/lawnchair/predictions/LawnchairPredictionEngine.kt`:
- Around line 129-135: The mergeRanked function has a performance issue where
checking membership with `key !in this` on the buildList result is O(n) for each
key, resulting in quadratic overall time complexity. Fix this by introducing a
LinkedHashSet to track seen keys separately before adding them to the result
list, which provides O(1) membership checks while maintaining insertion order.
Replace the current logic that checks `key !in this` with a check against the
LinkedHashSet, add the key to both the LinkedHashSet and the result list when
it's not seen, and return the buildList result as before.
🪄 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: CHILL
Plan: Pro
Run ID: 298d8a3b-723f-45e0-8c7d-7ef13ee9278e
📒 Files selected for processing (19)
build.gradlelawnchair/res/values/config.xmllawnchair/res/values/strings.xmllawnchair/src/app/lawnchair/LawnchairLauncher.ktlawnchair/src/app/lawnchair/predictions/LawnchairAppPredictor.ktlawnchair/src/app/lawnchair/predictions/LawnchairModelDelegate.ktlawnchair/src/app/lawnchair/predictions/LawnchairPredictionEngine.ktlawnchair/src/app/lawnchair/predictions/LawnchairPredictionManager.ktlawnchair/src/app/lawnchair/predictions/LawnchairPredictionStore.ktlawnchair/src/app/lawnchair/predictions/PredictionAppKey.ktlawnchair/src/app/lawnchair/predictions/PredictionEventResolver.ktlawnchair/src/app/lawnchair/predictions/UsageStatsRanker.ktlawnchair/src/app/lawnchair/preferences2/PreferenceManager2.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerPreferences.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/DismissedPredictionAppsPreferences.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/ExperimentalFeaturesPreferences.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/PredictionsPreferences.ktquickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.javasrc/com/android/launcher3/dagger/LauncherBaseAppComponent.java
✅ Files skipped from review due to trivial changes (2)
- lawnchair/res/values/config.xml
- lawnchair/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (7)
- lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt
- lawnchair/src/app/lawnchair/predictions/PredictionAppKey.kt
- lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerPreferences.kt
- lawnchair/src/app/lawnchair/LawnchairLauncher.kt
- lawnchair/src/app/lawnchair/predictions/LawnchairModelDelegate.kt
- lawnchair/src/app/lawnchair/ui/preferences/destinations/ExperimentalFeaturesPreferences.kt
- quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lawnchair/res/values/strings.xml`:
- Line 207: The string resource named app_predictions_description contains a
grammar error. In the string value "Show list of app that may be relevant to
you.", change "list" to "a list" (adding the article "a") and change "app" to
"apps" (making it plural to match the context). The corrected string should read
"Show a list of apps that may be relevant to you."
🪄 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: CHILL
Plan: Pro
Run ID: 45519190-f93c-4361-a1c3-8be20bfbf28c
📒 Files selected for processing (12)
build.gradlelawnchair/res/values/strings.xmllawnchair/src/app/lawnchair/predictions/LawnchairAppPredictor.ktlawnchair/src/app/lawnchair/predictions/LawnchairPredictionEngine.ktlawnchair/src/app/lawnchair/predictions/LawnchairPredictionManager.ktlawnchair/src/app/lawnchair/predictions/LawnchairPredictionStore.ktlawnchair/src/app/lawnchair/predictions/PredictionAppKey.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerPreferences.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/DismissedPredictionAppsPreferences.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/GeneralPreferences.ktlawnchair/src/app/lawnchair/ui/preferences/destinations/PredictionsPreferences.ktquickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
💤 Files with no reviewable changes (2)
- lawnchair/src/app/lawnchair/predictions/PredictionAppKey.kt
- lawnchair/src/app/lawnchair/ui/preferences/destinations/GeneralPreferences.kt
🚧 Files skipped from review as they are similar to previous changes (7)
- lawnchair/src/app/lawnchair/ui/preferences/destinations/DismissedPredictionAppsPreferences.kt
- lawnchair/src/app/lawnchair/predictions/LawnchairPredictionManager.kt
- lawnchair/src/app/lawnchair/predictions/LawnchairPredictionStore.kt
- build.gradle
- lawnchair/src/app/lawnchair/predictions/LawnchairPredictionEngine.kt
- quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
- lawnchair/src/app/lawnchair/predictions/LawnchairAppPredictor.kt
…pprediction Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
Description
Lawnchair rootless app prediction system. (Basic app ranking... Basic?)
Fixes #4235
Todo:
QuickstepModelDelegateHeadphone store for hotseat + allappsNo longer planned for this PR since that would likely be too much work.Allow changing prediction "influencer"Headphone store and other device state store is supposed to be the "influencer" but now that it's no longer planned, this will not be implemented.Reasoning
Code side
Heads up note: All of the files were written before AOSP Proto files synced with the IDE so there may be some weird usage aroundAll files were cleaned up now.LauncherAtomas I use AI with pretty much "Claude, integrateLauncherAtomwith my code, make no mistake (the IDE is lying to you about missing files)"This changes override predictor from
QuickstepModelDelegatewhen predictor mode is set to Lawnchair instead of System.The Lawnchair predictor a basic ranking system based on how many time the app was opened, it tried to be very accurate to how the system predictor returns it datas (because it has to be accepted by the AOSP, thanks AI for doing that painful tedious work) and works by listening in on app launch and event stats returned by the
StatsLogs.If a data from a store is not available like hotseat is non existent it will use random data (ranked lowest) and data from other store like allapps (ranked higher than random).
Compared to old prediction system in used by Lawnchair 10 this one does not take wired/wireless headphone into context. And since most of the changes there are not salavagable/technically infeasible we would have to write one from scratch.
UI side
The feature will be part of Prediction which will house all future Lawnchair prediction feature there, I envision it to also include feature like folder naming suggestions.
The switch act as global toggle in case some people don't like prediction features and wish to turn it off in one go.
screen-20260517-022344-1778959404906.mp4
The main switch on the top of the page will also act as global toggle.
Note
The animation will NOT be included in this PR, to restore this change revert bd5a06b, bd5a06b, then 81d0438
Cut from this PR // Animation rationale
UI side // Animation rationale
While animation will not be included in the PR, in case it is ever needed in the future.
The animation is originally designed with Lottie but there are some limitations that I personally feels like Android Animated Vector Drawable (AVD) is easier so it was ported to AVD. I designed in a way that it can be easily repeated or trick user that it's respond to preference signal by looping the animation when the feature is on, or stop looping when the feature is off.
Unfortunately there are some downside with the animation is that:
Testing
Install the latest build from this PR or build locally, on first use the predictor will randomly generate a list of suggested apps to use. which you can see when you open the allapps, hotseat, and widget selector.
The datastore in allapps and hotseat are separate meaning allapps will only show prediction for app that you open from allapps, and hotseat show prediction for app you open from hotseat.
However in an event where the user didn't have data on one of the store like say user don't have on allapps, the predictor will fallback to grabbing data from other store if exist then fallback to random data if data didn't exist at all but they will always be ranked lower than what's in allapps store.
When you have usage stats turn on for Lawnchair, instead of grabbing random data, Lawnchair will grab the best apps user is likely to open from usage stats instead.
Type of change
❌ Bug fix (A non-breaking change that fixes an issue)
✅ New feature (A non-breaking change that adds functionality)
❌ Breaking change (A fix or feature that would cause existing functionality to not work as expected)
❌ Refactor (A code change that neither fixes a bug nor adds a feature)
❌ Performance (A code change that improves performance)
❌ Style (Code style changes)
❌ Docs (Changes to documentation)
❌ Chore (Changes to the build process or other tooling)
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes