Skip to content

feat!: experimental Rive runtime backend (iOS + Android)#134

Open
mfazekas wants to merge 86 commits intomainfrom
feat/rive-ios-experimental
Open

feat!: experimental Rive runtime backend (iOS + Android)#134
mfazekas wants to merge 86 commits intomainfrom
feat/rive-ios-experimental

Conversation

@mfazekas
Copy link
Copy Markdown
Collaborator

@mfazekas mfazekas commented Jan 23, 2026

Adds a new native backend using Rive's experimental runtime APIs on both iOS and Android. The new backend is async-native — all ViewModel operations go through a CommandQueue, eliminating the need for blockingAsync/runBlocking wrappers on the non-deprecated API surface.

The experimental backend is now the default. Legacy backend files are moved to ios/legacy/ and android/src/legacy/ (identical to main except getEnums() stub and backend property). New implementations live in ios/new/ and android/src/new/. CI runs tests on both backends.

Release-please configured for beta prereleases (0.5.0-beta, published as @next).

Opting into the legacy backend

# iOS
USE_RIVE_LEGACY=1 pod install

# Android — add to gradle.properties
USE_RIVE_LEGACY=true

Without the flag, the experimental backend is used.

What works

  • ViewModel data binding (all property types: number, string, boolean, color, enum, trigger, image, list, artboard)
  • Property listeners (number, string, boolean, color, enum, trigger)
  • Nested ViewModels (viewModelAsync)
  • List operations (add, remove, swap, getInstanceAt)
  • Touch/pointer events (handled automatically by RiveUIView on iOS, custom implementation on Android)
  • play() / pause() (iOS: toggles isPaused; Android: fully implemented)
  • getEnums() for introspection
  • getPropertyCountAsync / getInstanceCountAsync

Known limitations

Android

  • defaultArtboardViewModel doesn't expose the ViewModel name — pending rive-android#443. This causes modelName/propertyCount/instanceCount to throw and viewModelAsync path validation to be skipped on those instances.
  • replaceViewModel is a no-op (not yet implemented)

iOS

  • reset() only pauses — doesn't actually reset the state machine

Both platforms

  • Deprecated legacy APIs (Rive Events, SMI inputs, text runs) are not available in the experimental runtime and throw when called
  • Image and list property listeners are no-ops (not yet available in experimental SDK)

@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch 2 times, most recently from 95816cf to 4fe9e12 Compare January 23, 2026 11:23
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch 4 times, most recently from 8485f9f to 9b4acd8 Compare February 9, 2026 10:25
@mfazekas mfazekas changed the title feat: experimental iOS API support (getEnums via SPM) feat: experimental iOS API support Feb 9, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

ktlint

🚫 [ktlint] standard:multiline-if-else reported by reviewdog 🐶
Missing { ... }

data[2] == 0x54.toByte() && data[3] == 0x4F.toByte()) return AssetType.FONT


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'


🚫 [ktlint] standard:try-catch-finally-spacing reported by reviewdog 🐶
Expected a newline after '{'


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'


🚫 [ktlint] standard:try-catch-finally-spacing reported by reviewdog 🐶
Expected a newline before '}'


🚫 [ktlint] standard:try-catch-finally-spacing reported by reviewdog 🐶
Expected a newline after '{'


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'


🚫 [ktlint] standard:try-catch-finally-spacing reported by reviewdog 🐶
Expected a newline before '}'


🚫 [ktlint] standard:no-unused-imports reported by reviewdog 🐶
Unused import


🚫 [ktlint] standard:string-template reported by reviewdog 🐶
Redundant curly braces

Log.d(TAG, "onSurfaceTextureAvailable: ${w}x${h} worker=${this@RiveReactNativeView.riveWorker != null}")


🚫 [ktlint] standard:if-else-wrapping reported by reviewdog 🐶
Expected a newline

val deltaTime = if (lastFrameTimeNs == 0L) Duration.ZERO


🚫 [ktlint] standard:multiline-if-else reported by reviewdog 🐶
Missing { ... }

val deltaTime = if (lastFrameTimeNs == 0L) Duration.ZERO


🚫 [ktlint] standard:if-else-wrapping reported by reviewdog 🐶
Expected a newline

else (frameTimeNanos - lastFrameTimeNs).nanoseconds


🚫 [ktlint] standard:multiline-if-else reported by reviewdog 🐶
Missing { ... }

else (frameTimeNanos - lastFrameTimeNs).nanoseconds


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:string-template reported by reviewdog 🐶
Redundant curly braces

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:max-line-length reported by reviewdog 🐶
Exceeded max line length (140)

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:string-template reported by reviewdog 🐶
Redundant curly braces

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Missing newline before ")"

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'

val worker = riveWorker ?: run { Log.w(TAG, "touch: no worker"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after ';'

val worker = riveWorker ?: run { Log.w(TAG, "touch: no worker"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'

val worker = riveWorker ?: run { Log.w(TAG, "touch: no worker"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'

val smHandle = stateMachineHandle ?: run { Log.w(TAG, "touch: no smHandle"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after ';'

val smHandle = stateMachineHandle ?: run { Log.w(TAG, "touch: no smHandle"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'

val smHandle = stateMachineHandle ?: run { Log.w(TAG, "touch: no smHandle"); return }


🚫 [ktlint] standard:if-else-wrapping reported by reviewdog 🐶
A single line if-statement should be kept simple. The 'THEN' may not be wrapped in a block.

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:string-template reported by reviewdog 🐶
Redundant curly braces

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after ';'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

val legacyFile = app.rive.runtime.kotlin.core.File(bytes)


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:max-line-length reported by reviewdog 🐶
Exceeded max line length (140)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Missing newline before ")"

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:function-naming reported by reviewdog 🐶
Function name should start with a lowercase letter (except factory methods) and use camel case


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:max-line-length reported by reviewdog 🐶
Exceeded max line length (140)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Missing newline before ")"

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")

@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from dd06b3a to a5855c9 Compare February 16, 2026 10:41
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

ktlint

🚫 [ktlint] standard:if-else-wrapping reported by reviewdog 🐶
A single line if-statement should be kept simple. The 'THEN' may not be wrapped in a block.

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:string-template reported by reviewdog 🐶
Redundant curly braces

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after ';'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

val legacyFile = app.rive.runtime.kotlin.core.File(bytes)


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:max-line-length reported by reviewdog 🐶
Exceeded max line length (140)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Missing newline before ")"

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:function-naming reported by reviewdog 🐶
Function name should start with a lowercase letter (except factory methods) and use camel case


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:max-line-length reported by reviewdog 🐶
Exceeded max line length (140)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Missing newline before ")"

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")

@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from 8134a07 to ec12673 Compare February 17, 2026 12:11
@mfazekas mfazekas changed the title feat: experimental iOS API support WIP feat: experimental iOS / Android POC Feb 17, 2026
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch 2 times, most recently from 91e2fb6 to cfb2ff6 Compare February 19, 2026 13:50
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from cfb2ff6 to 44681b5 Compare February 26, 2026 13:57
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch 2 times, most recently from f1b851e to aa2fdf0 Compare March 16, 2026 09:39
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch 4 times, most recently from 35c6fea to 4f1ab3f Compare March 27, 2026 15:52
@mfazekas mfazekas changed the base branch from main to feat/hooks-undefined-initial-value March 27, 2026 15:52
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from 4e0c53a to be6d334 Compare March 30, 2026 12:39
@mfazekas mfazekas force-pushed the feat/hooks-undefined-initial-value branch from 164180e to 7116ac7 Compare March 30, 2026 18:17
Base automatically changed from feat/hooks-undefined-initial-value to main March 30, 2026 18:20
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from dcdde5e to 0a3d09c Compare March 31, 2026 05:33
@mfazekas mfazekas changed the title WIP feat: experimental iOS / Android POC feat: experimental Rive runtime backend (iOS + Android) Mar 31, 2026
@mfazekas mfazekas marked this pull request as ready for review March 31, 2026 07:31
@mfazekas mfazekas changed the title feat: experimental Rive runtime backend (iOS + Android) feat!: experimental Rive runtime backend (iOS + Android) Mar 31, 2026
@mfazekas mfazekas requested a review from HayesGordon March 31, 2026 14:18
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from 5c7cd87 to e9ee276 Compare April 8, 2026 13:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 74 out of 153 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +174 to +178
try {
vmi = source.createInstanceByName(instanceName);
} catch {
// experimental backend throws for non-existent names
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block around source.createInstanceByName() swallows all exceptions with no logging. This can hide unexpected backend errors (not just “instance not found”) and make debugging very difficult. Consider logging the caught error (like the RiveFile path above) or narrowing the catch to only the expected “not found” failure mode.

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +91
/**
* Get all enums defined in this Rive file.
* Useful for debugging and building dynamic UIs.
* @experimental Uses the experimental Rive API on iOS
*/
getEnums(): Promise<RiveEnumDefinition[]>;
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getEnums() doc comment says it uses the experimental iOS API, but the method is also implemented on Android (including legacy) and is stubbed to throw on iOS legacy. Consider updating the doc to accurately describe backend/platform support (e.g., supported on experimental iOS + Android; throws on iOS legacy).

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +31
// Null when constructed via DefaultForArtboard — the Rive Android SDK does not expose
// the ViewModel name from a ViewModelInstance, so name-dependent operations are unavailable.
// Track: https://github.com/rive-app/rive-android/issues/XXX
private val viewModelName: String?,
private val parentFile: HybridRiveFile,
private val vmSource: ViewModelSource
) : HybridViewModelSpec() {
companion object {
private const val TAG = "HybridViewModel"
private const val NO_NAME_ERROR =
"This operation requires the ViewModel name, which is unavailable for ViewModels " +
"obtained via defaultArtboardViewModel(). The Rive Android SDK does not yet expose " +
"the ViewModel name from a ViewModelInstance. Use a named ViewModel instead, or " +
"track the upstream fix: https://github.com/rive-app/rive-android/issues/XXX"
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NO_NAME_ERROR (and the preceding comment) contains a placeholder upstream link (.../issues/XXX). Since this string is thrown to JS as an exception, it should reference the real upstream issue/PR (e.g. rive-android#443) or omit the link to avoid sending users to a dead URL.

Copilot uses AI. Check for mistakes.
val vmSource = ViewModelSource.DefaultForArtboard(artboard)
// Name is null because the Rive Android SDK does not expose the ViewModel name
// from a ViewModelInstance — name-dependent operations will throw UnsupportedOperationException.
// Track upstream: https://github.com/rive-app/rive-android/issues/XXX
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment references a placeholder upstream URL (.../issues/XXX). Since it documents a known limitation, it should point to the real tracking issue/PR so future maintainers/users can follow progress.

Suggested change
// Track upstream: https://github.com/rive-app/rive-android/issues/XXX

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +23
/**
* Reproduces issue #159 — Rive graphics stutter when JS/UI thread is under heavy load.
*
* Loads vehicles.riv from URL (endless animation).
* Two buttons: block JS thread or block UI thread for ~60s.
* If the vehicles stop animating, rendering depends on that thread.
*/

const VEHICLES_URL = require('../../../assets/rive/rewards.riv');

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file/constant naming and comment are inconsistent: the header says this loads vehicles.riv from a URL, but VEHICLES_URL is a local require() of rewards.riv. Consider either switching to the intended remote vehicles URL or updating the comment/constant name so the reproducer matches what it actually loads.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants