v2 SDK testing-pass fixes: telemetry, Flutter, React Native, Web#511
v2 SDK testing-pass fixes: telemetry, Flutter, React Native, Web#511Siddhesh2377 wants to merge 13 commits into
Conversation
…size-gate, telemetry poll-queue
…nts from lifecycle path
…ension, RAG top_k/retrieval_time, LoRA base_model_id (no proto/regen)
… LLM telemetry from tool-calling turns, propagate baseURL to model-assignment bridge
…elemetry auth token resolution
…/voice/download telemetry, framework + session_id
…oRA model tag, LoRA telemetry + layout fixes
… refresh), live download progress via async http_download slot, VLM pthread-pool deadlock, RAG + vision download UI, prebuilt ONNX/sherpa WASM vendoring
📝 WalkthroughWalkthroughThis PR refreshes example apps across Android, Flutter, React Native, and web, updates React Native navigation and themed UI, expands SDK telemetry and download instrumentation, and adds Flutter bridge isolate-based native calls plus a voice-agent mic driver. ChangesAndroid, Flutter, and web example updates
React Native example restructure
Commons telemetry and download instrumentation
Flutter plugin transport and bridge updates
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. 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 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Committed API key in source
- Removed the hardcoded React Native example API key and now only uses backend credentials saved in Settings.
- ✅ Fixed: RAG UI stale after reload fail
- Reset the loaded document, conversation, question, and thinking state when document loading fails after pipeline teardown.
Or push these changes by commenting:
@cursor push 620c1a3f76
Preview (620c1a3f76)
diff --git a/examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart b/examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
--- a/examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
+++ b/examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
@@ -97,6 +97,12 @@
_llmSupportsThinking = llmModel.supportsThinking;
} catch (e) {
_error = e.toString();
+ _documentName = null;
+ _isDocumentLoaded = false;
+ _messages = [];
+ _currentQuestion = '';
+ _lastResult = null;
+ _llmSupportsThinking = false;
// Tear down any partially-created pipeline so a failed ingest doesn't
// leave an orphaned C++ pipeline session behind.
await RunAnywhere.rag.destroyPipeline();
diff --git a/examples/react-native/RunAnywhereAI/App.tsx b/examples/react-native/RunAnywhereAI/App.tsx
--- a/examples/react-native/RunAnywhereAI/App.tsx
+++ b/examples/react-native/RunAnywhereAI/App.tsx
@@ -86,13 +86,6 @@
} from './src/screens/SettingsScreen';
import { logDiagnostic } from './src/utils/diagnostics';
-// Default backend config for the development Railway backend (mirrors the
-// Android example's gitignored local.properties → BuildConfig.RUNANYWHERE_*).
-// Used when no custom configuration has been saved in Settings.
-const DEFAULT_BASE_URL =
- 'https://runanywhere-backend-development.up.railway.app';
-const DEFAULT_API_KEY = 'runa_prod_f_sf4FrG4LhuUia4vupidcuIx5fD0t-usz2eNqKBeQw';
-
type InitState = 'loading' | 'ready' | 'error';
const InitializationErrorView: React.FC<{
@@ -163,9 +156,9 @@
const hasCustomConfig = await hasCustomConfiguration();
const effectiveApiKey =
- hasCustomConfig && customApiKey ? customApiKey : DEFAULT_API_KEY;
+ hasCustomConfig && customApiKey ? customApiKey : '';
const effectiveBaseURL =
- hasCustomConfig && customBaseURL ? customBaseURL : DEFAULT_BASE_URL;
+ hasCustomConfig && customBaseURL ? customBaseURL : '';
if (
hasUsableBackendConfig({You can send follow-ups to the cloud agent here.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 2bbba92. Configure here.
| // Used when no custom configuration has been saved in Settings. | ||
| const DEFAULT_BASE_URL = | ||
| 'https://runanywhere-backend-development.up.railway.app'; | ||
| const DEFAULT_API_KEY = 'runa_prod_f_sf4FrG4LhuUia4vupidcuIx5fD0t-usz2eNqKBeQw'; |
There was a problem hiding this comment.
Committed API key in source
High Severity
The React Native example now embeds a full DEFAULT_API_KEY string in App.tsx and uses it whenever Settings has no custom key. That exposes a production-style credential in git history and lets any clone call the Railway backend without local secrets, unlike the Android/Flutter examples that keep keys out of the repo.
Reviewed by Cursor Bugbot for commit 2bbba92. Configure here.
| _error = e.toString(); | ||
| // Tear down any partially-created pipeline so a failed ingest doesn't | ||
| // leave an orphaned C++ pipeline session behind. | ||
| await RunAnywhere.rag.destroyPipeline(); |
There was a problem hiding this comment.
RAG UI stale after reload fail
Medium Severity
When loadDocument fails after a document was already loaded, the new catch calls destroyPipeline() but leaves _isDocumentLoaded and related UI fields from the previous success. The screen still looks loaded while the native pipeline is gone, so queries fail until the user clears state manually.
Reviewed by Cursor Bugbot for commit 2bbba92. Configure here.
There was a problem hiding this comment.
Actionable comments posted: 2
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (9)
examples/react-native/RunAnywhereAI/App.tsx (2)
159-209:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReplace
console.logcalls with allowed logging APIs.Lines 176/185/192/206 use
console.log, which violates the React Native example logging policy.As per coding guidelines, “Remove console.log statements; only console.warn and console.error are allowed.”
🤖 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 `@examples/react-native/RunAnywhereAI/App.tsx` around lines 159 - 209, Replace all four console.log calls in the initialization block with console.warn or console.error as appropriate per coding guidelines. Specifically, change the console.log for '[App] Found backend configuration', '[App] SDK initialized with backend configuration (staging)', '[App] SDK initialized in DEVELOPMENT mode', and the final SDK initialization details log to use console.warn for informational messages (or console.error if these represent error conditions). Only console.warn and console.error are permitted; console.log is not allowed.Source: Coding guidelines
122-142:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReintroduce optional Genie backend registration in App init.
Lines 122-142 now register only LlamaCPP and ONNX, so Genie backend availability is never probed/registered in
App.tsx.🧩 Suggested direction
async function registerBackends(): Promise<BackendRegistrationState> { const llamaResult = LlamaCPP ? await LlamaCPP.register() : false; const llamaRegistered = llamaResult !== false; + const genieResult = Genie ? await Genie.register() : false; + const genieRegistered = genieResult !== false; const onnxResult = ONNX ? await ONNX.register() : false; const onnxRegistered = onnxResult !== false; - return { llamaRegistered, onnxRegistered }; + return { llamaRegistered, genieRegistered, onnxRegistered }; }As per coding guidelines, “Use dynamic require() with try/catch for optional backend registration (LlamaCPP, Genie, ONNX) in App.tsx initialization flow” and “Filter Genie NPU backend models per Qualcomm chip ID … Android platform only.”
🤖 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 `@examples/react-native/RunAnywhereAI/App.tsx` around lines 122 - 142, The registerBackends() function is currently missing Genie backend registration. Add Genie backend registration following the same pattern as LlamaCPP and ONNX backends, using dynamic require() with try/catch for optional backend loading. Include logic to filter Genie NPU backend models based on Qualcomm chip ID for Android platform only, similar to how llamaRegistered and onnxRegistered are determined. Add appropriate diagnostic logging for Genie backend registration status and include the Genie registration state in the returned BackendRegistrationState object.Source: Coding guidelines
examples/react-native/RunAnywhereAI/react-native.config.js (1)
11-19:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMissing autolinking disables for New Architecture compatibility.
Per the coding guidelines, this config must disable autolinking for
react-native-live-audio-stream(iOS only),react-native-audio-recorder-player,react-native-sound, andreact-native-ttsto prevent New Architecture/Nitrogen incompatibility issues. As per coding guidelines, "Disable autolinking in react-native.config.js for: react-native-live-audio-stream (iOS), react-native-audio-recorder-player (both), react-native-sound (both), react-native-tts (both) due to New Architecture/Nitrogen incompatibility".🔧 Proposed fix to add required autolinking disables
dependencies: { // Nitro modules requires Turbo codegen for iOS (NitroModulesSpec.h) 'react-native-nitro-modules': { platforms: { android: null, ios: {}, }, }, + // Disable autolinking for packages incompatible with New Architecture/Nitrogen + 'react-native-live-audio-stream': { + platforms: { + ios: null, + }, + }, + 'react-native-audio-recorder-player': { + platforms: { + android: null, + ios: null, + }, + }, + 'react-native-sound': { + platforms: { + android: null, + ios: null, + }, + }, + 'react-native-tts': { + platforms: { + android: null, + ios: null, + }, + }, },🤖 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 `@examples/react-native/RunAnywhereAI/react-native.config.js` around lines 11 - 19, The dependencies object in react-native.config.js is missing required autolinking disables for packages incompatible with New Architecture/Nitrogen. Add entries in the dependencies object for react-native-live-audio-stream (with ios disabled), react-native-audio-recorder-player (with both android and ios disabled), react-native-sound (with both android and ios disabled), and react-native-tts (with both android and ios disabled), following the same pattern as the existing react-native-nitro-modules configuration where disabled platforms are set to null and enabled ones to an empty object.Source: Coding guidelines
examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx (1)
531-535:⚠️ Potential issue | 🟠 MajorUse manual async iteration instead of aggregateStream in ChatScreen.
Line 534-539 uses
aggregateStream()which is the streaming helper approach. Per the ChatScreen coding guideline, you must use manual async iteration (viaSymbol.asyncIterator()) forgenerateStream()on Android due to Hermes incompatibility with NitroModules custom async iterables.Reference the pattern used in VLMService.ts and VoiceAssistantScreen.tsx:
const iter = stream[Symbol.asyncIterator](); let result = await iter.next(); while (!result.done) { const event = result.value; // handle event result = await iter.next(); }🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx` around lines 531 - 535, Replace the call to RunAnywhere.aggregateStream() with manual async iteration using Symbol.asyncIterator() to ensure Hermes compatibility on Android. After getting the eventStream from RunAnywhere.generateStream(), instead of passing it to aggregateStream(), manually iterate through the stream by calling Symbol.asyncIterator() on eventStream, then repeatedly call next() on the iterator in a while loop until done is true, handling each event value as it arrives. Follow the exact pattern demonstrated in VLMService.ts and VoiceAssistantScreen.tsx where the iterator is created, result checked for done status, and the event is extracted from result.value before calling next() again.Source: Coding guidelines
examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx (1)
350-363:⚠️ Potential issue | 🟠 Major | ⚡ Quick winLive mode drops earlier finalized phrases.
At Line 360/Line 361, each finalized segment replaces the prior transcript instead of appending to it, so only the latest phrase remains visible.
Suggested patch
- if (partial.isFinal || finalText) { - accumulatedTranscriptRef.current = text; - setTranscript(text); + if (partial.isFinal || finalText) { + const combined = accumulatedTranscriptRef.current + ? `${accumulatedTranscriptRef.current} ${text}` + : text; + accumulatedTranscriptRef.current = combined; + setTranscript(combined); setPartialTranscript(''); setConfidence(partial.finalOutput?.confidence ?? null); } else {🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx` around lines 350 - 363, In the consumeLiveTranscription function, the issue is that when a finalized segment is received (in the if block checking partial.isFinal or finalText), the code replaces the entire transcript with just the new text instead of appending to it. To fix this, modify the lines that set accumulatedTranscriptRef.current and setTranscript so that they append the new finalized text to the existing transcript value rather than replacing it completely. This will preserve all previously finalized phrases while adding the new one.examples/react-native/RunAnywhereAI/src/screens/BenchmarkScreen.tsx (1)
118-124:⚠️ Potential issue | 🟠 MajorReplace
for await...ofwith manual iterator consumption for SDK streams.Line 122 uses
for await...ofwithwarmupEvents(fromRunAnywhere.generateStream()), which violates the requirement to use manual async iteration for SDK async iterables due to Hermes incompatibility with NitroModules custom async iterables.Suggested patch
- for await (const event of warmupEvents) { - if (event.isFinal) break; - } + const warmupIterator = warmupEvents[Symbol.asyncIterator](); + try { + let step = await warmupIterator.next(); + while (!step.done) { + if (step.value.isFinal) break; + step = await warmupIterator.next(); + } + } finally { + await warmupIterator.return?.(); + }🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/BenchmarkScreen.tsx` around lines 118 - 124, The for await...of loop consuming the warmupEvents stream from RunAnywhere.generateStream() needs to be replaced with manual async iterator consumption to avoid Hermes incompatibility issues with NitroModules custom async iterables. Replace the for await...of construct with a while loop that manually calls the next() method on the iterator obtained from warmupEvents[Symbol.asyncIterator](), checking the done property in each iteration and accessing the value property to check event.isFinal until the condition is met.Source: Coding guidelines
examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx (1)
183-192:⚠️ Potential issue | 🟠 Major | ⚡ Quick winClean up the VAD stream if recording startup fails.
After
isListeningRef.current = true, any rejection fromstartRecordingleaves the stream open and the VAD consumer waiting while the UI still says it is not listening.Proposed fix
const stream = createPushableAudioStream(); streamRef.current = stream; isListeningRef.current = true; setLatestResult(null); setFrameCount(0); - taskRef.current = consumeVAD(RunAnywhere.streamVAD(stream.iterable)); - await capture.startRecording((chunk) => { - stream.push(pcm16ChunkToFloat32Bytes(chunk)); - }); - setIsListening(true); + try { + taskRef.current = consumeVAD(RunAnywhere.streamVAD(stream.iterable)); + await capture.startRecording((chunk) => { + stream.push(pcm16ChunkToFloat32Bytes(chunk)); + }); + setIsListening(true); + } catch (error) { + isListeningRef.current = false; + stream.close(); + await taskRef.current?.catch(() => {}); + streamRef.current = null; + taskRef.current = null; + Alert.alert('Listen Failed', `Failed to start VAD: ${error}`); + }🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx` around lines 183 - 192, The issue is that if capture.startRecording() fails after isListeningRef.current is set to true, the stream created by createPushableAudioStream() remains open and the VAD consumer task in taskRef.current is left waiting, creating a resource leak. Wrap the capture.startRecording() call in a try-catch block and in the catch handler, clean up by closing the stream stored in streamRef.current, cleaning up the VAD task in taskRef.current, resetting isListeningRef.current back to false, and then re-throwing the error so the UI properly reflects the failure state.examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx (1)
453-459:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove the new
console.logdiagnostics.These changed blocks add
console.logcalls in the React Native example. Keep diagnostics asconsole.warn/console.erroronly when actionable, or remove them before merge. As per coding guidelines,examples/react-native/RunAnywhereAI/**/*.{ts,tsx,js,jsx}: Remove console.log statements; only console.warn and console.error are allowed.Also applies to: 474-480, 595-602
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx` around lines 453 - 459, The console.log statement logging the generation settings (with temperature, maxTokens, systemPrompt, and thinkingModeEnabled properties) violates the React Native example coding guidelines which only allow console.warn and console.error. Remove this entire console.log call along with the other console.log calls mentioned at lines 474-480 and 595-602 in the same file, keeping only console.warn or console.error if there are actionable diagnostics that need to be preserved.Source: Coding guidelines
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart (1)
203-205:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDestroy Phase-1 telemetry managers even when Phase 2 never initialized.
attachSinkPhase1()creates_managerPtrwithout setting_isInitialized; if initialization is reset or fails before Phase 2, this guard returns early and leaves the C++ telemetry sink/manager attached.🧹 Proposed lifecycle fix
/// Shutdown telemetry manager static void shutdown() { - if (!_isInitialized || _managerPtr == null) return; + final managerPtr = _managerPtr; + if (managerPtr == null) { + _isInitialized = false; + return; + } try { final lib = PlatformLoader.loadCommons(); @@ final destroy = lib.lookupFunction<Void Function(Pointer<Void>), void Function(Pointer<Void>)>('rac_telemetry_manager_destroy'); - destroy(_managerPtr!); + destroy(managerPtr); _managerPtr = null; _isInitialized = false; _logger.debug('Telemetry manager shutdown');Also applies to: 214-222
🤖 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 `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart` around lines 203 - 205, The shutdown() method guards against cleanup using both _isInitialized and _managerPtr == null conditions, but since attachSinkPhase1() creates _managerPtr without setting _isInitialized, the early return prevents Phase-1 managers from being destroyed when Phase-2 initialization fails. Fix the guard condition in shutdown() to only check if _managerPtr == null is true, removing the _isInitialized check, so that any created manager pointers are properly destroyed regardless of whether full initialization completed.
🟡 Minor comments (14)
examples/web/RunAnywhereAI/src/components/model-selection.ts-111-117 (1)
111-117:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSnapshot listeners before dispatch to avoid skipped callbacks.
Line 111 iterates
listenersdirectly. If a listener unsubscribes itself (or another listener) during execution,splicemutates the array and later listeners can be skipped in the same cycle.💡 Proposed fix
- for (const listener of listeners) { + for (const listener of [...listeners]) { try { listener(); } catch (err) { console.warn('[model-selection] hydrated listener threw', err); } }🤖 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 `@examples/web/RunAnywhereAI/src/components/model-selection.ts` around lines 111 - 117, The for loop iterating over the listeners array can skip callbacks because mutations to the listeners array (via splice when listeners unsubscribe) cause the iteration to become misaligned. Create a snapshot of the listeners array before the for loop that iterates over listener() calls, so that unsubscribe operations during iteration do not affect which listeners are executed in the current cycle.examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart-179-183 (1)
179-183:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard fire-and-forget teardown errors in
dispose().Line 182 starts an unawaited teardown with no error handler. If it fails, it can surface as an uncaught async exception during widget disposal.
Suggested fix
- unawaited(RunAnywhere.rag.destroyPipeline()); + unawaited( + RunAnywhere.rag.destroyPipeline().catchError((Object e, StackTrace st) { + debugPrint('RAG destroyPipeline in dispose failed: $e'); + }), + );🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart` around lines 179 - 183, The dispose() method in RagViewModel has a fire-and-forget async call to RunAnywhere.rag.destroyPipeline() without error handling, which can cause uncaught exceptions during widget disposal. Add error handling to the unawaited call by attaching a catch handler or using .onError to gracefully handle any exceptions that occur during the pipeline destruction. This ensures failures during teardown do not surface as uncaught async exceptions when the widget is disposed.examples/react-native/RunAnywhereAI/src/navigation/RootNavigator.tsx-36-67 (1)
36-67:⚠️ Potential issue | 🟡 MinorModal routes are declared but not registered in the navigation stack.
Lines 36-67 register tabs and feature screens, but
ModelSelection,Lora,ConversationList, andChatAnalyticsare declared inroutes.tsandnavigation.types.tswithout being registered as Stack.Screen components. While ChatScreen currently renders these as modal components using local state rather than navigation, this creates a mismatch between the declared navigation contracts and the actual implementation. If any code attempts to navigate to these routes (e.g.,navigate('ModelSelection')), React Navigation will not find a matching screen. Either register these as Stack.Screen components or remove them from the shared navigation contracts.🤖 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 `@examples/react-native/RunAnywhereAI/src/navigation/RootNavigator.tsx` around lines 36 - 67, The modal routes ModelSelection, Lora, ConversationList, and ChatAnalytics are declared in routes.ts and navigation.types.ts but are missing as Stack.Screen components in the RootNavigator Stack.Navigator. Either add these four routes as Stack.Screen components within the Stack.Navigator (similar to how VisionHubScreen, STTScreen, and other screens are registered), providing the appropriate component and options for each, or if these routes are only meant for local state-based modal rendering in ChatScreen, remove them from the shared navigation contracts in routes.ts and navigation.types.ts to eliminate the mismatch between declared and implemented routes.examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx-280-285 (1)
280-285:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winClose the model sheet only after a successful load.
Line 284 closes the sheet unconditionally, but
loadModel(Lines 293-349) swallows failures and returnsvoid, so failed loads still dismiss the picker.🐛 Suggested change
- const handleModelSelected = useCallback(async (model: SDKModelInfo) => { + const handleModelSelected = useCallback(async (model: SDKModelInfo) => { // The sheet shows its own Loading spinner during the await; once the model // is loaded we close it so the user lands directly in the chat. - await loadModel(model); - setShowModelSelection(false); - }, []); + const loaded = await loadModel(model); + if (loaded) setShowModelSelection(false); + }, [loadModel]); - const loadModel = async (model: SDKModelInfo) => { + const loadModel = async (model: SDKModelInfo): Promise<boolean> => { try { setIsModelLoading(true); ... if (!model.isDownloaded && !model.localPath) { Alert.alert( 'Error', 'Model has not been downloaded. Open the model picker to download it first.' ); - return; + return false; } ... if (result.success) { ... const tools = await RunAnywhere.getRegisteredTools(); setRegisteredToolCount(tools.length); + return true; } else { ... + return false; } } catch (error) { console.error('[ChatScreen] Error loading model:', error); Alert.alert('Error', `Failed to load model: ${error}`); + return false; } finally { setIsModelLoading(false); } };Also applies to: 293-349
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx` around lines 280 - 285, The handleModelSelected callback unconditionally closes the model selection sheet by calling setShowModelSelection(false) after awaiting loadModel, but since loadModel swallows failures and returns void, the sheet closes even when the model fails to load. Modify the loadModel function to indicate success or failure (either by returning a boolean success value or by throwing an error on failure), and update handleModelSelected to only call setShowModelSelection(false) when loadModel completes successfully. If loadModel throws an error, catch it and keep the sheet open so the user can try selecting another model.examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx-88-90 (1)
88-90:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd accessibility metadata to icon-only buttons.
The new-chat, clear-search, and delete controls are icon-only touch targets and should expose role/label/hint so screen readers can operate them reliably.
Proposed fix
- <TouchableOpacity onPress={handleCreate} hitSlop={8}> + <TouchableOpacity + onPress={handleCreate} + hitSlop={8} + accessibilityRole="button" + accessibilityLabel="Start new conversation" + > ... - <TouchableOpacity onPress={() => setSearchQuery('')} hitSlop={8}> + <TouchableOpacity + onPress={() => setSearchQuery('')} + hitSlop={8} + accessibilityRole="button" + accessibilityLabel="Clear search" + > ... - <TouchableOpacity + <TouchableOpacity style={styles.deleteBtn} onPress={() => handleDelete(c)} hitSlop={8} + accessibilityRole="button" + accessibilityLabel={`Delete conversation ${c.title}`} >Also applies to: 107-109, 184-190
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx` around lines 88 - 90, The icon-only button components (TouchableOpacity elements with Icon children for new-chat, clear-search, and delete controls) lack accessibility metadata needed for screen readers. Add accessibility properties to each of these TouchableOpacity components: set accessible to true, add accessibilityRole as "button", provide a descriptive accessibilityLabel for each button's purpose (e.g., "Create new chat", "Clear search", "Delete"), and optionally add accessibilityHint with additional context. Apply these changes to all three icon-only button instances mentioned: the new-chat button with handleCreate handler, the clear-search button around lines 107-109, and the delete button around lines 184-190.examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx-44-48 (1)
44-48:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winTrimmed search text is not actually used for filtering.
trim()is only used for the truthy check; the actual query passed tosearchConversations(...)still includes leading/trailing whitespace, which can produce false “No matches”.Proposed fix
- const filtered = useMemo( - () => - searchQuery.trim() ? searchConversations(searchQuery) : conversations, - [conversations, searchQuery, searchConversations] - ); + const filtered = useMemo(() => { + const query = searchQuery.trim(); + return query ? searchConversations(query) : conversations; + }, [conversations, searchQuery, searchConversations]);🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx` around lines 44 - 48, The searchQuery.trim() is only used for the boolean check in the useMemo hook, but the untrimmed searchQuery is still passed to searchConversations function, causing searches with leading/trailing whitespace to not work as expected. Store the trimmed query in a variable (e.g., const trimmedQuery = searchQuery.trim()) and use that trimmedQuery variable both in the conditional check and as the argument passed to searchConversations to ensure whitespace is consistently removed before filtering.examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx-113-113 (1)
113-113:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep conversation taps responsive while the keyboard is open.
Without
keyboardShouldPersistTaps, the first tap on a row often just dismisses the keyboard instead of selecting the conversation.Proposed fix
- <BottomSheetScrollView contentContainerStyle={styles.list}> + <BottomSheetScrollView + contentContainerStyle={styles.list} + keyboardShouldPersistTaps="handled" + >🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx` at line 113, Add the `keyboardShouldPersistTaps` prop to the `BottomSheetScrollView` component in ConversationListScreen to ensure that taps on conversation rows register properly even when the keyboard is open. Set this prop to "handled" or "always" (depending on your preference for when keyboard dismissal should be allowed) so that the first tap on a conversation row actually selects it instead of just dismissing the keyboard.sdk/runanywhere-commons/src/infrastructure/network/environment.cpp-61-66 (1)
61-66:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix contradictory telemetry-gate comment.
The comment says telemetry is sent in every environment, but Line 66 excludes development. Please align the wording with the actual condition to avoid future gate regressions.
🤖 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 `@sdk/runanywhere-commons/src/infrastructure/network/environment.cpp` around lines 61 - 66, The multi-line comment describing the telemetry gate behavior contradicts the actual return condition. The comment states "telemetry is sent in every environment" but the return statement on the line with `env != RAC_ENV_DEVELOPMENT` explicitly excludes development from the gate. Update the comment to accurately clarify that this function determines whether telemetry requires the normal send gate with authentication (true for staging and production), while development is intentionally excluded because it uses a separate immediate-flush path to the local backend. This will prevent future confusion about what the gate controls and why development is excluded.sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp-328-338 (1)
328-338:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winEmit zero-valued inference steps consistently.
The comment says
steps=0is meaningful, but Line 336 still usesadd_int, which drops0. Use the always-emitting helper likeseedso imagegen rows do not lose that metric.Proposed fix
- json.add_int("num_inference_steps", payload->num_inference_steps); + json.add_int_always("num_inference_steps", payload->num_inference_steps);🤖 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 `@sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp` around lines 328 - 338, The num_inference_steps field on line 336 currently uses add_int which will drop zero values, but according to the comment zero values are meaningful for this metric. Change the add_int call for num_inference_steps to use add_int_always instead, consistent with how the seed field is handled on line 337, so that zero-valued inference steps are properly emitted in the telemetry data.examples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsx-121-126 (1)
121-126:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReset LoRA adapter state when the selected model changes while hidden.
Because
refresh()only runs whenvisibleis true,onAdaptersChanged([])does not fire whenmodelIdchanges while the sheet is closed, so the ChatScreen LoRA badge can keep showing the previous model’s adapter count.Proposed state reset
+ useEffect(() => { + setAvailableAdapters([]); + updateLoaded([]); + setError(null); + }, [modelId, updateLoaded]); + // Only refresh once the user actually opens the sheet — the sheet is mounted // (hidden) on the chat screen at startup, so an unconditional mount-refresh // would hit the LoRA service before any model is loaded. useEffect(() => { - if (visible) refresh(); + if (visible) { + void refresh(); + } }, [visible, refresh]);🤖 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 `@examples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsx` around lines 121 - 126, The useEffect hook in LoRASheet.tsx only refreshes when the sheet becomes visible, but this means the LoRA adapter state is not reset when the selected model changes while the sheet is hidden. Add modelId to the dependency array of the useEffect hook, and update the effect logic to call onAdaptersChanged([]) to clear the adapter state whenever modelId changes while the sheet is not visible, ensuring the ChatScreen LoRA badge accurately reflects the current model's adapter count.examples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsx-132-144 (1)
132-144:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd an accessibility label to the icon-only send/stop button.
The primary composer action currently renders only an icon or stop square, so assistive tech can announce it as an unlabeled button.
Proposed accessibility update
<TouchableOpacity style={[ styles.action, { backgroundColor: canPressAction ? colors.primary : colors.surfaceVariant, }, ]} onPress={handleAction} disabled={!canPressAction} activeOpacity={0.8} + accessibilityRole="button" + accessibilityLabel={canStop ? 'Stop generating' : 'Send message'} + accessibilityState={{ disabled: !canPressAction }} >🤖 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 `@examples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsx` around lines 132 - 144, The TouchableOpacity component for the send/stop button lacks an accessibility label, preventing screen readers from announcing its purpose to users. Add an accessibilityLabel prop to the TouchableOpacity component that dynamically provides a descriptive label based on the button's current state (e.g., "Send message" when canPressAction is true, and "Stop" when false). This will ensure assistive technologies can properly announce the button's function to users relying on accessibility features.examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx-1084-1089 (1)
1084-1089:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winWire the Documentation row or remove it.
The row is tappable but its
onPressbody is empty, so users get no feedback when tapping Documentation.Do you want me to wire this to the project docs URL or open an issue to track the missing destination?
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx` around lines 1084 - 1089, The TouchableOpacity component wrapping the Documentation Text in SettingsScreen has an empty onPress handler that provides no functionality when tapped. Either implement the onPress callback to open the project documentation URL (using a method like Linking.openURL or navigation), or remove the entire TouchableOpacity and Documentation Text row if the documentation link is not needed for this screen.examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx-96-106 (1)
96-106:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winBound the activity log in state, not only at render time.
activityLog.slice(0, 20)limits display, but every speech transition is still retained in memory for long listening sessions.Proposed fix
setActivityLog((prev) => [ { type: 'started', timestampMs: Date.now() }, ...prev, - ]); + ].slice(0, 20)); } else if (!speechDetected && prevSpeechRef.current) { setActivityLog((prev) => [ { type: 'ended', timestampMs: Date.now() }, ...prev, - ]); + ].slice(0, 20));Also applies to: 409-409
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx` around lines 96 - 106, The activity log state is unbounded and grows indefinitely with every speech transition event, even though it is only sliced to 20 items at display time. In the useEffect hook where setActivityLog is called for both speech 'started' and 'ended' events, apply bounds directly to the state update by limiting the array size when creating the new log entry (for example, using slice to keep only the most recent entries in the prev array before prepending the new entry). This ensures the memory footprint remains constant during long listening sessions, rather than relying solely on display-time slicing.sdk/runanywhere-flutter/packages/runanywhere/lib/features/voice_agent/services/voice_agent_mic_driver.dart-105-109 (1)
105-109:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winForward recorder stream errors to subscribers.
The mic stream
onErroronly logs, leavingeventsopen and silent after a runtime capture failure. Surface the error and stop the driver so the UI can exit the listening state.🐛 Proposed fix
_micSub = stream.listen( _onChunk, - onError: (Object e, StackTrace st) => _logger.warning('Mic error: $e'), + onError: (Object e, StackTrace st) { + _logger.warning('Mic error: $e'); + if (!_out.isClosed) { + _out.addError(e, st); + } + unawaited(stop()); + }, cancelOnError: false, );🤖 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 `@sdk/runanywhere-flutter/packages/runanywhere/lib/features/voice_agent/services/voice_agent_mic_driver.dart` around lines 105 - 109, The onError handler in the _micSub stream listener only logs the error but does not notify subscribers or stop the driver, leaving the voice agent in a broken state. Modify the onError callback to forward the error to the events sink so subscribers are notified of the failure, and call the appropriate cleanup or stop method to halt the driver. This allows the UI to detect the error and exit the listening state properly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4f6ab52c-ef30-4833-8ba9-a1d9f2b1e4ff
⛔ Files ignored due to path filters (26)
examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-hdpi/ic_launcher.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-mdpi/ic_launcher.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xhdpi/ic_launcher.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngis excluded by!**/*.pngexamples/flutter/RunAnywhereAI/assets/fonts/figtree.ttfis excluded by!**/*.ttfexamples/flutter/RunAnywhereAI/assets/fonts/figtree_italic.ttfis excluded by!**/*.ttfexamples/flutter/RunAnywhereAI/assets/fonts/maple_mono.ttfis excluded by!**/*.ttfexamples/flutter/RunAnywhereAI/lib/generated/solutions_yaml.dartis excluded by!**/generated/**examples/react-native/RunAnywhereAI/android/app/src/main/assets/fonts/Figtree-Italic.ttfis excluded by!**/*.ttfexamples/react-native/RunAnywhereAI/android/app/src/main/assets/fonts/Figtree.ttfis excluded by!**/*.ttfexamples/react-native/RunAnywhereAI/android/app/src/main/assets/fonts/MapleMono.ttfis excluded by!**/*.ttfexamples/react-native/RunAnywhereAI/assets/fonts/Figtree-Italic.ttfis excluded by!**/*.ttfexamples/react-native/RunAnywhereAI/assets/fonts/Figtree.ttfis excluded by!**/*.ttfexamples/react-native/RunAnywhereAI/assets/fonts/MapleMono.ttfis excluded by!**/*.ttfyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (134)
examples/android/RunAnywhereAI/app/build.gradle.ktsexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.ktexamples/flutter/RunAnywhereAI/.gitignoreexamples/flutter/RunAnywhereAI/android/app/src/main/AndroidManifest.xmlexamples/flutter/RunAnywhereAI/android/app/src/main/res/drawable/ic_launcher_foreground.xmlexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xmlexamples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xmlexamples/flutter/RunAnywhereAI/android/app/src/main/res/values-night/styles.xmlexamples/flutter/RunAnywhereAI/android/app/src/main/res/values/styles.xmlexamples/flutter/RunAnywhereAI/android/build.gradleexamples/flutter/RunAnywhereAI/android/gradle/wrapper/gradle-wrapper.propertiesexamples/flutter/RunAnywhereAI/android/settings.gradleexamples/flutter/RunAnywhereAI/lib/app/content_view.dartexamples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dartexamples/flutter/RunAnywhereAI/lib/core/services/model_catalog_bootstrap.dartexamples/flutter/RunAnywhereAI/lib/core/utilities/constants.dartexamples/flutter/RunAnywhereAI/lib/features/benchmarks/llm_benchmark_provider.dartexamples/flutter/RunAnywhereAI/lib/features/benchmarks/tts_benchmark_provider.dartexamples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_list_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dartexamples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/solutions/solutions_view.dartexamples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dartexamples/flutter/RunAnywhereAI/lib/features/vision/vlm_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/voice/voice_agent_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/voice/voice_component_view_model_base.dartexamples/react-native/RunAnywhereAI/App.tsxexamples/react-native/RunAnywhereAI/android/app/build.gradleexamples/react-native/RunAnywhereAI/android/gradle.properties.exampleexamples/react-native/RunAnywhereAI/android/link-assets-manifest.jsonexamples/react-native/RunAnywhereAI/android/settings.gradleexamples/react-native/RunAnywhereAI/babel.config.jsexamples/react-native/RunAnywhereAI/index.jsexamples/react-native/RunAnywhereAI/ios/RunAnywhereAI.xcodeproj/project.pbxprojexamples/react-native/RunAnywhereAI/ios/RunAnywhereAI/Info.plistexamples/react-native/RunAnywhereAI/ios/RunAnywhereAITests/Info.plistexamples/react-native/RunAnywhereAI/ios/link-assets-manifest.jsonexamples/react-native/RunAnywhereAI/metro.config.jsexamples/react-native/RunAnywhereAI/package.jsonexamples/react-native/RunAnywhereAI/react-native.config.jsexamples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsxexamples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsxexamples/react-native/RunAnywhereAI/src/components/chat/MessageBubble.tsxexamples/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsxexamples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsxexamples/react-native/RunAnywhereAI/src/components/ui/BottomSheet.tsxexamples/react-native/RunAnywhereAI/src/features/chat/components/ChatHeader.tsxexamples/react-native/RunAnywhereAI/src/features/chat/components/PromptSuggestions.tsxexamples/react-native/RunAnywhereAI/src/features/intro/IntroScreen.tsxexamples/react-native/RunAnywhereAI/src/navigation/BottomTabs.tsxexamples/react-native/RunAnywhereAI/src/navigation/RootNavigator.tsxexamples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsxexamples/react-native/RunAnywhereAI/src/navigation/navigation.types.tsexamples/react-native/RunAnywhereAI/src/navigation/routes.tsexamples/react-native/RunAnywhereAI/src/navigation/transitions.tsexamples/react-native/RunAnywhereAI/src/screens/BenchmarkScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/RAGScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/STTScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/SolutionsScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/VADScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/VisionHubScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsxexamples/react-native/RunAnywhereAI/src/services/ModelCatalogBootstrap.tsexamples/react-native/RunAnywhereAI/src/theme/system/ThemeProvider.tsxexamples/react-native/RunAnywhereAI/src/theme/system/colors.tsexamples/react-native/RunAnywhereAI/src/theme/system/dimens.tsexamples/react-native/RunAnywhereAI/src/theme/system/icons.tsxexamples/react-native/RunAnywhereAI/src/theme/system/index.tsexamples/react-native/RunAnywhereAI/src/theme/system/motion.tsexamples/react-native/RunAnywhereAI/src/theme/system/typography.tsexamples/react-native/RunAnywhereAI/src/types/index.tsexamples/web/RunAnywhereAI/src/components/model-selection.tsexamples/web/RunAnywhereAI/src/main.tsexamples/web/RunAnywhereAI/src/views/documents.tsexamples/web/RunAnywhereAI/src/views/vision.tspackage.jsonsdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.hsdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.hsdk/runanywhere-commons/src/core/events.cppsdk/runanywhere-commons/src/features/diffusion/diffusion_module.cppsdk/runanywhere-commons/src/features/embeddings/embeddings_module.cppsdk/runanywhere-commons/src/features/llm/llm_module.cppsdk/runanywhere-commons/src/features/llm/tool_calling_session.cppsdk/runanywhere-commons/src/features/lora/rac_lora_service.cppsdk/runanywhere-commons/src/features/rag/rac_rag_proto_abi.cppsdk/runanywhere-commons/src/features/stt/stt_module.cppsdk/runanywhere-commons/src/features/tts/tts_module.cppsdk/runanywhere-commons/src/features/vad/vad_module.cppsdk/runanywhere-commons/src/features/vlm/vlm_module.cppsdk/runanywhere-commons/src/features/voice_agent/voice_agent_d7_abi.cppsdk/runanywhere-commons/src/features/voice_agent/voice_agent_internal_helpers.cppsdk/runanywhere-commons/src/features/voice_agent/voice_agent_internal_helpers.hsdk/runanywhere-commons/src/features/voice_agent/voice_agent_proto_abi.cppsdk/runanywhere-commons/src/infrastructure/device/rac_device_manager.cppsdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cppsdk/runanywhere-commons/src/infrastructure/network/environment.cppsdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cppsdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cppsdk/runanywhere-flutter/packages/runanywhere/android/src/main/kotlin/com/runanywhere/sdk/httptransport/OkHttpHttpTransport.ktsdk/runanywhere-flutter/packages/runanywhere/lib/features/voice_agent/services/voice_agent_mic_driver.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_auth.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_embeddings.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_llm.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_stt.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_tool_calling.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_tts.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_vlm.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_voice_agent.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_embeddings.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_llm.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_rag.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_stt.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tools.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tts.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_voice.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dartsdk/runanywhere-react-native/packages/core/src/Foundation/Constants/SDKConstants.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+TextGeneration.tssdk/runanywhere-react-native/packages/core/src/native/NitroModulesGlobalInit.tssdk/runanywhere-web/packages/core/src/runtime/PlatformAdapter.tssdk/runanywhere-web/wasm/CMakeLists.txtsdk/runanywhere-web/wasm/scripts/vendor-onnxruntime-wasm.shsdk/runanywhere-web/wasm/scripts/vendor-sherpa-onnx-wasm.sh
💤 Files with no reviewable changes (7)
- examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
- examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
- examples/flutter/RunAnywhereAI/android/app/src/main/res/drawable/ic_launcher_foreground.xml
- examples/react-native/RunAnywhereAI/src/services/ModelCatalogBootstrap.ts
- examples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsx
- examples/react-native/RunAnywhereAI/android/gradle.properties.example
- examples/react-native/RunAnywhereAI/android/settings.gradle
| android:label="RunAnywhere AI" | ||
| android:name="${applicationName}" | ||
| android:icon="@mipmap/ic_launcher" | ||
| android:roundIcon="@mipmap/ic_launcher_round" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
MANIFEST="examples/flutter/RunAnywhereAI/android/app/src/main/AndroidManifest.xml"
echo "Checking $MANIFEST"
rg -n 'extractNativeLibs|uses-native-library|libcdsprpc\.so' "$MANIFEST"Repository: RunanywhereAI/runanywhere-sdks
Length of output: 289
Fix extractNativeLibs attribute value in AndroidManifest.xml.
The file contains both required entries, but extractNativeLibs is set to "false" (line 12) when the coding guidelines require it to be "true". The <uses-native-library> entry for libcdsprpc.so is correct (line 15). Change line 12 to use android:extractNativeLibs="true".
🤖 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 `@examples/flutter/RunAnywhereAI/android/app/src/main/AndroidManifest.xml` at
line 11, Locate the android:extractNativeLibs attribute in the
AndroidManifest.xml file and change its value from "false" to "true" to comply
with the coding guidelines. This attribute controls whether native libraries
should be extracted from the APK, and it needs to be enabled for proper native
library handling.
Source: Coding guidelines
| Future<RAGStatistics> ingestDocumentAsync(RAGDocument document) async { | ||
| final session = _requireSession(); | ||
| if (RacNative.bindings.rac_rag_ingest_proto == null) { | ||
| throw UnsupportedError('rac_rag_ingest_proto is unavailable'); | ||
| } | ||
| final sessionAddr = session.address; | ||
| final requestBytes = document.writeToBuffer(); | ||
| final resultBytes = | ||
| await Isolate.run(() => _ragIngestWorker(sessionAddr, requestBytes)); |
There was a problem hiding this comment.
Guard the RAG session lifetime while worker isolates are running.
Both async methods capture session.address and then yield to Isolate.run; during that await, destroyPipeline() can free the native session, leaving the worker with a dangling pointer and a potential native crash. Serialize in-flight RAG operations and make destroy wait for them, or add a native ref-count/lifecycle guard before passing the pointer across isolates.
🛡️ Sketch of the required guard
class DartBridgeRAG {
DartBridgeRAG._();
static final DartBridgeRAG shared = DartBridgeRAG._();
ffi.Pointer<ffi.Void>? _session;
bool _registered = false;
+ Future<void> _operationTail = Future<void>.value();
+ Future<T> _runWithLiveSession<T>(
+ Future<T> Function(int sessionAddr) operation,
+ ) {
+ final sessionAddr = _requireSession().address;
+ final next = _operationTail.then((_) => operation(sessionAddr));
+ _operationTail = next.then<void>((_) {}, onError: (_) {});
+ return next;
+ }
+
Future<RAGStatistics> ingestDocumentAsync(RAGDocument document) async {
- final session = _requireSession();
if (RacNative.bindings.rac_rag_ingest_proto == null) {
throw UnsupportedError('rac_rag_ingest_proto is unavailable');
}
- final sessionAddr = session.address;
final requestBytes = document.writeToBuffer();
- final resultBytes =
- await Isolate.run(() => _ragIngestWorker(sessionAddr, requestBytes));
+ final resultBytes = await _runWithLiveSession(
+ (sessionAddr) => Isolate.run(
+ () => _ragIngestWorker(sessionAddr, requestBytes),
+ ),
+ );
return RAGStatistics.fromBuffer(resultBytes);
}Apply the same guard to queryAsync, and have the destroy path await or otherwise block on _operationTail before destroying the native session.
Also applies to: 175-183
🤖 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 `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dart`
around lines 122 - 130, The ingestDocumentAsync and queryAsync methods capture
session.address and then yield to Isolate.run(), creating a window where
destroyPipeline() can be called and free the native session while the worker
still holds a dangling pointer. To fix this, implement a serialization mechanism
using _operationTail (a Future that tracks the last in-flight operation): wrap
each Isolate.run() call in both ingestDocumentAsync and queryAsync with a guard
that chains the new operation to _operationTail, update _operationTail to
complete after the isolate finishes, and modify destroyPipeline() to await
_operationTail before destroying the native session to ensure all pending RAG
operations complete first.
|
@coderabbitai please do a detailed review of the PR |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Look at key high-complexity files mentioned in AI summaries
# 1. Check the telemetry manager changes for thread safety
rg -n "http_wakeup\|poll_http_request\|queue\|mutex" sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp | head -60
# 2. Check voice agent turn metrics helper for potential issues
cat sdk/runanywhere-commons/src/features/voice_agent/voice_agent_internal_helpers.h 2>/dev/null | head -80
# 3. Check the web download driver for potential resource leaks
rg -n "web_download_driver\|free\|delete\|cancel\|abort" sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp | head -60
# 4. Check the Flutter telemetry phase-1 attach for init ordering
rg -n "attachSinkPhase1\|initializeServices\|phase1\|Phase1\|Phase 1" sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart 2>/dev/null | head -40
# 5. Check React Native BottomSheet implementation for gesture handler dependency
cat examples/react-native/RunAnywhereAI/src/components/ui/BottomSheet.tsx 2>/dev/null | head -90
# 6. Check environment.cpp for the telemetry gate change
cat sdk/runanywhere-commons/src/infrastructure/network/environment.cpp 2>/dev/null
# 7. Check dart_bridge_device.dart for OS platform reporting changes
cat sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dart 2>/dev/null
# 8. Check the VLM pthread-pool deadlock fix
rg -n "pthread\|deadlock\|thread_pool\|main_thread" sdk/runanywhere-commons/src/features/vlm/vlm_module.cpp 2>/dev/null | head -30
# 9. Check rag_view_model.dart for the dispose leak fix
cat examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart 2>/dev/null | head -80
# 10. Check voice_component_view_model_base.dart for the lifecycle loop fix
rg -n "resolveLoadedModel\|applyCurrentModelSnapshot\|modelLifecycle\|loaded" examples/flutter/RunAnywhereAI/lib/features/voice/voice_component_view_model_base.dart 2>/dev/null | head -50Length of output: 47537 🏁 Script executed: #!/bin/bash
# Look at more specific areas
# Check dart_bridge_rag.dart for potential issues with isolate pointer passing
cat sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dart 2>/dev/null
# Check tool_calling_session.cpp for telemetry emission
cat sdk/runanywhere-commons/src/features/llm/tool_calling_session.cpp 2>/dev/null | head -80
# Check the VoiceAgentMicDriver for barge-in/drop logic
rg -n "drop\|barge\|isProcessing\|turnRequest\|stop\|cancel" sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_stt.dart 2>/dev/null | head -30
# Check ChatScreen.tsx for toolsEnabled storage key
rg -n "TOOL_CALLING_ENABLED\|APP_STORAGE_KEYS\|toolsEnabled\|AsyncStorage" examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx 2>/dev/null | head -50
# Check for potential missing null check in dart_bridge_auth.dart
cat sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_auth.dart 2>/dev/null | head -80
# Check VLM camera view dispose order fix
cat examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart 2>/dev/null | head -60
# Check the telemetry_manager.cpp for session_id fallback logic
rg -n "sdk_session_id\|session_id\|fallback" sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp 2>/dev/null | head -40
# Check download_orchestrator.cpp for the web async driver
rg -n "web_download_driver\|http_download\|async\|cooperative_cancel\|abort_once" sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp 2>/dev/null | head -80
# Check rac_telemetry_manager.h for poll_http_request framing docs
cat sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h 2>/dev/null
# Check the Flutter model lifecycle event loop fix
cat examples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dart 2>/dev/null | head -80Length of output: 30814 🏁 Script executed: #!/bin/bash
# Deep dive into specific critical paths
# 1. Full dart_bridge_telemetry.dart
cat sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart 2>/dev/null
# 2. Voice agent d7 abi - turn metrics guard
cat sdk/runanywhere-commons/src/features/voice_agent/voice_agent_d7_abi.cpp 2>/dev/null | head -120
# 3. VoiceAgentMicDriver full file
cat sdk/runanywhere-flutter/packages/runanywhere/lib/features/voice_agent/services/voice_agent_mic_driver.dart 2>/dev/null
# 4. Check model_selection.ts web SDK for hydrated event subscription
cat examples/web/RunAnywhereAI/src/components/model-selection.ts 2>/dev/null | head -80
# 5. Check download_orchestrator.cpp new driver section
rg -n "web_download_driver\|start_proto_download_worker\|async.*driver\|http_download.*avail" sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp 2>/dev/null | head -50
# 6. Check react-native index.js import order
cat examples/react-native/RunAnywhereAI/index.js 2>/dev/null
# 7. Check App.tsx for staging vs production env handling
rg -n "SDK_ENVIRONMENT\|hasUsableBackendConfig\|effectiveApiKey\|staging" examples/react-native/RunAnywhereAI/App.tsx 2>/dev/null | head -50
# 8. Check the dart_bridge_llm.dart changes for sendable closure fix
cat sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_llm.dart 2>/dev/null | head -80
# 9. Check the BottomSheet.tsx snapPoints conversion
rg -n "snapPoints\|containerHeight\|getHeight\|percentage" examples/react-native/RunAnywhereAI/src/components/ui/BottomSheet.tsx 2>/dev/null
# 10. Check for any missing exports/imports in theme system index.ts
cat examples/react-native/RunAnywhereAI/src/theme/system/index.ts 2>/dev/nullLength of output: 39891 🏁 Script executed: #!/bin/bash
# Check PR diff stats and get additional context
# 1. Check if there are test files for new functionality
fd -e test.dart -e spec.ts -e test.cpp . | grep -E "(telemetry|voice_agent|download|rag|embeddings)" | head -20
# 2. Check the proto ABI for session_id propagation in voice agent
rg -n "session_id\|sessionId" sdk/runanywhere-commons/src/features/voice_agent/ 2>/dev/null | head -30
# 3. Check for null pointer safety in voice_agent_internal_helpers
rg -n "nullptr\|null.*check\|if.*session_id\|if.*model_id" sdk/runanywhere-commons/src/features/voice_agent/voice_agent_internal_helpers.cpp 2>/dev/null | head -30
# 4. Check the vad_module.cpp key logic for accumulator flush
rg -n "flush\|open.*utterance\|utterance.*state\|VadUtteranceState" sdk/runanywhere-commons/src/features/vad/vad_module.cpp 2>/dev/null | head -40
# 5. Check the ConversationListScreen for visible prop passed correctly
rg -n "ConversationListScreen\|visible.*conversation\|conversation.*visible" examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx 2>/dev/null | head -20
# 6. Check for memory leaks in embeddings isolate worker
rg -n "free\|finally\|RacProtoBuffer\|bindings\|rac_proto_buffer" sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_embeddings.dart 2>/dev/null | head -40
# 7. Check DefaultConfig usage in runanywhere_ai_app.dart
rg -n "DefaultConfig\|runanywhereApiKey\|runanywhereBaseUrl\|hasCustomConfig" examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart 2>/dev/null | head -30
# 8. Check if rac_auth_get_access_token is declared/exported in web SDK
rg -rn "rac_auth_get_access_token" . | head -20
# 9. Check the telemetry_manager for queue thread safety
rg -n "queue\|mutex\|lock\|http_queue\|enqueue" sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp 2>/dev/null | head -40
# 10. Check diff stat of the PR (lines changed)
gh pr view 511 --repo RunanywhereAI/runanywhere-sdks --json additions,deletions,changedFiles 2>/dev/null || echo "PR stats not available"Length of output: 3593 🏁 Script executed: Length of output: 91 Here are the results from shell you requested: |
…WAV playback fix, flutter VLM/voice redesign
… 5.4.0, gradle wrapper 9.6.0
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
examples/flutter/RunAnywhereAI/lib/features/vision/vlm_view_model.dart (1)
88-126: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winCancel/guard streaming work when the ViewModel is disposed.
describeSelectedImage()can keep receiving tokens after the screen is popped, but the owning widget disposes thisChangeNotifier. Add a disposed guard and cancel any in-flight VLM generation ondispose()to avoid updates after teardown and leaked native work.🛡️ Proposed fix
bool _isProcessing = false; String _currentDescription = ''; String? _error; String? _selectedImagePath; String _prompt = 'Describe this image in detail.'; + bool _disposed = false; @@ - notifyListeners(); + _notify(); @@ - notifyListeners(); + _notify(); @@ - notifyListeners(); + _notify(); @@ - notifyListeners(); + _notify(); @@ - notifyListeners(); + _notify(); @@ buffer.write(event.token); _currentDescription = buffer.toString(); - notifyListeners(); + _notify(); } @@ debugPrint('VLM describe error: $e'); _error = e.toString(); - notifyListeners(); + _notify(); } finally { _isProcessing = false; - notifyListeners(); + _notify(); } } + + void _notify() { + if (!_disposed) { + notifyListeners(); + } + } + + `@override` + void dispose() { + _disposed = true; + if (_isProcessing) { + unawaited( + sdk.RunAnywhere.vlm.cancelVLMGeneration().catchError((Object e) { + debugPrint('VLM cancel during dispose failed: $e'); + }), + ); + } + super.dispose(); + }Also applies to: 131-134
🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_view_model.dart` around lines 88 - 126, The describeSelectedImage() method can continue receiving and processing tokens even after the ViewModel is disposed, causing UI updates and potential memory leaks. Override the dispose() method in this ChangeNotifier class to cancel any in-flight VLM generation work. Within the describeSelectedImage() method's await for loop that processes tokens from the stream, add a guard check to break early if the ViewModel has been disposed. Store a reference to the stream subscription so it can be cancelled in dispose(). Apply the same disposal guard pattern to the related method on lines 131-134 to ensure consistent cleanup across all streaming operations.examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx (3)
157-185: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winGuard against double-starting the mic session.
isSessionActiveis only set afterdriver.start()resolves, so rapid taps can start multipleVoiceAgentMicDriverinstances and overwritemicDriverRef, leaving an earlier recorder outside cleanup ownership.Suggested fix
const micDriverRef = useRef<VoiceAgentMicDriver | null>(null); const scrollRef = useRef<ScrollView>(null); + const startInFlightRef = useRef(false); @@ const handleToggleSession = useCallback(async () => { - if (status === VoicePipelineStatus.Processing || status === VoicePipelineStatus.Thinking) { + if ( + startInFlightRef.current || + status === VoicePipelineStatus.Processing || + status === VoicePipelineStatus.Thinking + ) { return; } @@ } try { + startInFlightRef.current = true; // Compose the pipeline against the loaded models. ensureVAD defaults to // true → loads silero-vad so the C++ agent uses the model VAD. await RunAnywhere.initializeVoiceAgentWithLoadedModels(); @@ } catch (error) { console.error('[VoiceAssistant] Failed to start voice agent:', error); await cleanupVoiceSession(); Alert.alert('Error', `Failed to start voice agent: ${error}`); + } finally { + startInFlightRef.current = false; } }, [isSessionActive, allModelsLoaded, status, appendTurn, phaseToStatus, cleanupVoiceSession]);Also applies to: 203-210
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx` around lines 157 - 185, The handleToggleSession callback is vulnerable to race conditions where rapid taps can start multiple VoiceAgentMicDriver instances before isSessionActive is set to true. Since isSessionActive is only updated after driver.start() resolves, the guard clause checking isSessionActive at the beginning of handleToggleSession cannot prevent re-entry. Fix this by setting isSessionActive to true immediately before awaiting driver.start(), so that subsequent rapid taps will be blocked by the existing guard condition. This ensures that only one VoiceAgentMicDriver instance is active and prevents orphaned recorders from being left outside cleanup ownership.
167-171: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winInclude VAD in the required-model alert.
Line 57 now requires
vadModel, but the alert still tells users only STT, LLM, and TTS are required.Suggested fix
Alert.alert( 'Models Required', - 'Please load all required models (STT, LLM, TTS) to use the voice assistant.' + 'Please load all required models (STT, LLM, TTS, VAD) to use the voice assistant.' );🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx` around lines 167 - 171, The alert message displayed when allModelsLoaded is false is outdated and does not mention the VAD model requirement that was added on line 57. Update the Alert.alert call with title 'Models Required' to include VAD in the list of required models. Modify the message string to reference all four required models: STT, LLM, TTS, and VAD instead of just the original three.
424-439: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAdd accessibility metadata to icon-only buttons.
The mic and clear controls render only icons, so screen-reader users do not get actionable labels. Add roles, labels, and disabled state.
Suggested fix
<TouchableOpacity style={[styles.micButton, { backgroundColor: micButtonColor() }]} onPress={handleToggleSession} disabled={!micButtonEnabled} activeOpacity={0.8} + accessibilityRole="button" + accessibilityLabel={isSessionActive ? 'Stop voice assistant' : 'Start voice assistant'} + accessibilityState={{ disabled: !micButtonEnabled }} > @@ {conversation.length > 0 && ( - <TouchableOpacity onPress={handleClear} hitSlop={8} style={styles.clearBtn}> + <TouchableOpacity + onPress={handleClear} + hitSlop={8} + style={styles.clearBtn} + accessibilityRole="button" + accessibilityLabel="Clear voice conversation" + > <Icon name="trash" size={dimens.icon.sm} color={colors.onSurfaceVariant} /> </TouchableOpacity> )}🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx` around lines 424 - 439, The mic button (TouchableOpacity with onPress={handleToggleSession}) and clear button (TouchableOpacity with onPress={handleClear}) are icon-only controls that lack accessibility metadata, making them unusable for screen reader users. Add the accessible prop set to true, an accessibilityLabel prop describing the button's purpose (such as "Toggle voice session" for the mic button and "Clear conversation" for the clear button), and an accessibilityState prop with the disabled state to the mic button to reflect its disabled prop value.
🧹 Nitpick comments (2)
examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart (1)
38-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
ListenableBuilderinstead of manual ViewModel listener plumbing.This screen still registers a listener and calls
setStatefor ViewModel updates. Wrap theScaffoldinListenableBuilderand remove_onViewModelChanged/listener cleanup.♻️ Proposed refactor
_viewModel = VLMViewModel(); _promptController = TextEditingController(text: _viewModel.prompt); - _viewModel.addListener(_onViewModelChanged); unawaited(_viewModel.checkModelStatus()); } - void _onViewModelChanged() { - if (mounted) { - setState(() {}); - } - } - `@override` void dispose() { - _viewModel.removeListener(_onViewModelChanged); _viewModel.dispose(); _promptController.dispose(); super.dispose(); } `@override` Widget build(BuildContext context) { - return Scaffold( - appBar: _buildAppBar(), - body: _viewModel.isModelLoaded - ? _buildMainContent() - : _buildModelRequiredContent(), + return ListenableBuilder( + listenable: _viewModel, + builder: (context, _) { + return Scaffold( + appBar: _buildAppBar(), + body: _viewModel.isModelLoaded + ? _buildMainContent() + : _buildModelRequiredContent(), + ); + }, ); }As per coding guidelines,
examples/flutter/RunAnywhereAI/lib/**/*.dart: “Use ChangeNotifier + ListenableBuilder pattern for feature-level state management.”🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart` around lines 38 - 62, Remove the manual listener plumbing from the VlmCameraView class. Delete the _viewModel.addListener(_onViewModelChanged) call from initState, remove the _onViewModelChanged method entirely, and remove the _viewModel.removeListener(_onViewModelChanged) call from dispose. In the build method, wrap the Scaffold widget and all its content with ListenableBuilder, passing _viewModel as the listenable parameter and moving the current Scaffold structure into the builder callback function. This aligns with the ChangeNotifier + ListenableBuilder pattern guidelines.Source: Coding guidelines
examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx (1)
347-441: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftMove inline style objects out of the render tree.
The new JSX adds many inline object literals for theme values. Please hoist these into a themed style factory or memoized style map so the screen follows the React Native example styling rule and avoids repeated per-render style allocations.
As per coding guidelines,
examples/react-native/RunAnywhereAI/**/*.{ts,tsx}should “Avoid inline styles; use centralized theme system (colors.ts, typography.ts, spacing.ts) instead.”🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx` around lines 347 - 441, The render method contains numerous inline style objects (like backgroundColor, paddingTop, borderRadius, textAlign, etc.) that are recreated on every render, causing unnecessary performance overhead. Extract all inline style definitions from the JSX into a memoized style factory using useMemo that depends on the theme dependencies (colors, typography, dimens, insets). Create a single styles object within useMemo that returns an organized map of all styles needed for the setupCard, divider, conversationArea, emptyState, scrollView, error message area, controls section, micButton, and clearBtn elements. Then replace each inline style object in the View and Text components with references to the corresponding style from this centralized memoized styles map.Source: Coding guidelines
🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart`:
- Around line 184-189: The _pickImage method performs an asynchronous operation
with the image picker, and if the user navigates away while the picker is open,
the widget can be disposed before the picker returns. Add a check for the
mounted property before calling _viewModel.setSelectedImage(xFile.path) to
ensure the widget is still active and part of the widget tree before attempting
to update the ViewModel. This prevents attempting to update a disposed object.
- Around line 72-82: The Text widget displaying _viewModel.loadedModelName! can
overflow the AppBar when model names are long. Wrap the Text widget in a
SizedBox with a maxWidth constraint to limit the width, and add overflow:
TextOverflow.ellipsis to the Text widget's styling to handle text overflow
gracefully with an ellipsis. This will prevent the model name from overflowing
the AppBar actions row while maintaining readability.
In `@examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart`:
- Around line 267-277: After setup is complete (when allModelsLoaded is true),
the active Voice Models menu displays STT, LLM, and TTS model controls but
currently omits VAD. Locate the section in the code where the Voice Models
menu/model-info is rendered after allModelsLoaded becomes true, and add a
_buildModelConfigRow call for VAD using _viewModel.currentVADModel,
_viewModel.vadModelState, the VAD color (AppColors.statusOrange), and the
_showVADModelSelection handler, mirroring the VAD configuration row shown during
the initial setup phase.
- Around line 584-628: The bubble UI in the voice_assistant_view.dart file lacks
semantic information for screen readers to announce the speaker role. Wrap the
Text widget displaying turn.text with a Semantics widget that includes a label
parameter to announce the speaker identity, such as "User message" or "Assistant
message" based on the isUser variable, ensuring that assistive technology users
can understand who is speaking even without visual color and alignment cues.
In `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`:
- Around line 59-62: Store the timeout ID from the delayed error reset (around
lines 188-191) in a ref, and then clear that timeout in the cleanupVoiceSession
function before setting micDriverRef.current to null. This prevents the timeout
from firing after cleanup and resetting the screen state to "Listening" when the
session has already stopped or the component has unmounted.
---
Outside diff comments:
In `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_view_model.dart`:
- Around line 88-126: The describeSelectedImage() method can continue receiving
and processing tokens even after the ViewModel is disposed, causing UI updates
and potential memory leaks. Override the dispose() method in this ChangeNotifier
class to cancel any in-flight VLM generation work. Within the
describeSelectedImage() method's await for loop that processes tokens from the
stream, add a guard check to break early if the ViewModel has been disposed.
Store a reference to the stream subscription so it can be cancelled in
dispose(). Apply the same disposal guard pattern to the related method on lines
131-134 to ensure consistent cleanup across all streaming operations.
In `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`:
- Around line 157-185: The handleToggleSession callback is vulnerable to race
conditions where rapid taps can start multiple VoiceAgentMicDriver instances
before isSessionActive is set to true. Since isSessionActive is only updated
after driver.start() resolves, the guard clause checking isSessionActive at the
beginning of handleToggleSession cannot prevent re-entry. Fix this by setting
isSessionActive to true immediately before awaiting driver.start(), so that
subsequent rapid taps will be blocked by the existing guard condition. This
ensures that only one VoiceAgentMicDriver instance is active and prevents
orphaned recorders from being left outside cleanup ownership.
- Around line 167-171: The alert message displayed when allModelsLoaded is false
is outdated and does not mention the VAD model requirement that was added on
line 57. Update the Alert.alert call with title 'Models Required' to include VAD
in the list of required models. Modify the message string to reference all four
required models: STT, LLM, TTS, and VAD instead of just the original three.
- Around line 424-439: The mic button (TouchableOpacity with
onPress={handleToggleSession}) and clear button (TouchableOpacity with
onPress={handleClear}) are icon-only controls that lack accessibility metadata,
making them unusable for screen reader users. Add the accessible prop set to
true, an accessibilityLabel prop describing the button's purpose (such as
"Toggle voice session" for the mic button and "Clear conversation" for the clear
button), and an accessibilityState prop with the disabled state to the mic
button to reflect its disabled prop value.
---
Nitpick comments:
In `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart`:
- Around line 38-62: Remove the manual listener plumbing from the VlmCameraView
class. Delete the _viewModel.addListener(_onViewModelChanged) call from
initState, remove the _onViewModelChanged method entirely, and remove the
_viewModel.removeListener(_onViewModelChanged) call from dispose. In the build
method, wrap the Scaffold widget and all its content with ListenableBuilder,
passing _viewModel as the listenable parameter and moving the current Scaffold
structure into the builder callback function. This aligns with the
ChangeNotifier + ListenableBuilder pattern guidelines.
In `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`:
- Around line 347-441: The render method contains numerous inline style objects
(like backgroundColor, paddingTop, borderRadius, textAlign, etc.) that are
recreated on every render, causing unnecessary performance overhead. Extract all
inline style definitions from the JSX into a memoized style factory using
useMemo that depends on the theme dependencies (colors, typography, dimens,
insets). Create a single styles object within useMemo that returns an organized
map of all styles needed for the setupCard, divider, conversationArea,
emptyState, scrollView, error message area, controls section, micButton, and
clearBtn elements. Then replace each inline style object in the View and Text
components with references to the corresponding style from this centralized
memoized styles map.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 62592583-ee59-4ea2-b4c8-fb1a5ab1e8eb
📒 Files selected for processing (12)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/voice/VoiceScreen.ktexamples/android/RunAnywhereAI/gradle/libs.versions.tomlexamples/android/RunAnywhereAI/gradle/wrapper/gradle-wrapper.propertiesexamples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dartexamples/flutter/RunAnywhereAI/lib/features/vision/vlm_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/voice/voice_agent_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dartexamples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsxsdk/runanywhere-flutter/packages/runanywhere/lib/features/tts/services/audio_playback_manager.dartsdk/runanywhere-react-native/packages/core/src/Features/VoiceSession/AudioPlaybackManager.tssdk/runanywhere-react-native/packages/core/src/Features/VoiceSession/VoiceAgentMicDriver.tssdk/runanywhere-react-native/packages/core/src/index.ts
✅ Files skipped from review due to trivial changes (1)
- examples/android/RunAnywhereAI/gradle/wrapper/gradle-wrapper.properties
| if (_viewModel.loadedModelName != null) | ||
| Padding( | ||
| padding: const EdgeInsets.only(right: AppSpacing.large), | ||
| child: Center( | ||
| Center( | ||
| child: Padding( | ||
| padding: const EdgeInsets.symmetric(horizontal: AppSpacing.small), | ||
| child: Text( | ||
| _viewModel.loadedModelName!, | ||
| style: AppTypography.caption(context).copyWith( | ||
| color: Colors.grey, | ||
| ), | ||
| style: AppTypography.caption(context) | ||
| .copyWith(color: AppColors.textSecondary(context)), | ||
| ), | ||
| ), | ||
| ), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Constrain the AppBar model label.
Long model names/IDs can overflow the AppBar actions row. Add a width cap plus ellipsis.
💡 Proposed fix
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.small),
- child: Text(
- _viewModel.loadedModelName!,
- style: AppTypography.caption(context)
- .copyWith(color: AppColors.textSecondary(context)),
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 160),
+ child: Text(
+ _viewModel.loadedModelName!,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: AppTypography.caption(context)
+ .copyWith(color: AppColors.textSecondary(context)),
+ ),
),
),
),🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart`
around lines 72 - 82, The Text widget displaying _viewModel.loadedModelName! can
overflow the AppBar when model names are long. Wrap the Text widget in a
SizedBox with a maxWidth constraint to limit the width, and add overflow:
TextOverflow.ellipsis to the Text widget's styling to handle text overflow
gracefully with an ellipsis. This will prevent the model name from overflowing
the AppBar actions row while maintaining readability.
| Future<void> _pickImage(ImageSource source) async { | ||
| try { | ||
| final xFile = await _picker.pickImage(source: source); | ||
| if (xFile != null) { | ||
| _viewModel.setSelectedImage(xFile.path); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Check mounted after the OS picker returns.
If the user leaves the screen while the picker is open, this success path can update a disposed ViewModel.
🛡️ Proposed fix
final xFile = await _picker.pickImage(source: source);
+ if (!mounted) return;
if (xFile != null) {
_viewModel.setSelectedImage(xFile.path);
}🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart`
around lines 184 - 189, The _pickImage method performs an asynchronous operation
with the image picker, and if the user navigates away while the picker is open,
the widget can be disposed before the picker returns. Add a check for the
mounted property before calling _viewModel.setSelectedImage(xFile.path) to
ensure the widget is still active and part of the widget tree before attempting
to update the ViewModel. This prevents attempting to update a disposed object.
| const SizedBox(height: AppSpacing.large), | ||
|
|
||
| // VAD Model | ||
| _buildModelConfigRow( | ||
| icon: Icons.hearing, | ||
| label: 'Voice Activity (VAD)', | ||
| modelName: _viewModel.currentVADModel, | ||
| state: _viewModel.vadModelState, | ||
| color: AppColors.statusOrange, | ||
| onTap: _showVADModelSelection, | ||
| ), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Expose VAD controls after setup too.
Once allModelsLoaded is true, this setup view disappears, but the active “Voice Models” menu/model-info section still only shows STT, LLM, and TTS. Add VAD there as well so users can inspect or change the required VAD model without leaving the flow.
💡 Proposed UI wiring
ListTile(
leading: const Icon(
Icons.volume_up,
color: AppColors.primaryPurple,
),
title: const Text('Text-to-Speech'),
subtitle: Text(_viewModel.currentTTSModel),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.pop(context);
_showTTSModelSelection();
},
),
+ ListTile(
+ leading: const Icon(Icons.hearing, color: AppColors.statusOrange),
+ title: const Text('Voice Activity (VAD)'),
+ subtitle: Text(_viewModel.currentVADModel),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () {
+ Navigator.pop(context);
+ _showVADModelSelection();
+ },
+ ),
const SizedBox(height: AppSpacing.large), _ModelBadge(
icon: Icons.volume_up,
label: 'TTS',
value: _viewModel.currentTTSModel,
color: AppColors.primaryPurple,
),
+ _ModelBadge(
+ icon: Icons.hearing,
+ label: 'VAD',
+ value: _viewModel.currentVADModel,
+ color: AppColors.statusOrange,
+ ),🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart`
around lines 267 - 277, After setup is complete (when allModelsLoaded is true),
the active Voice Models menu displays STT, LLM, and TTS model controls but
currently omits VAD. Locate the section in the code where the Voice Models
menu/model-info is rendered after allModelsLoaded becomes true, and add a
_buildModelConfigRow call for VAD using _viewModel.currentVADModel,
_viewModel.vadModelState, the VAD color (AppColors.statusOrange), and the
_showVADModelSelection handler, mirroring the VAD configuration row shown during
the initial setup phase.
| const radius = AppSpacing.cornerRadiusBubble; | ||
|
|
||
| return Padding( | ||
| padding: const EdgeInsets.only(bottom: AppSpacing.large), | ||
| child: Column( | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| children: [ | ||
| Text( | ||
| speaker, | ||
| style: AppTypography.caption(context).copyWith( | ||
| color: AppColors.textSecondary(context), | ||
| fontWeight: FontWeight.w500, | ||
| ), | ||
| final bubble = ConstrainedBox( | ||
| constraints: BoxConstraints( | ||
| maxWidth: MediaQuery.of(context).size.width * 0.76, | ||
| ), | ||
| child: Container( | ||
| padding: const EdgeInsets.symmetric( | ||
| horizontal: AppSpacing.large, | ||
| vertical: AppSpacing.mediumLarge, | ||
| ), | ||
| decoration: BoxDecoration( | ||
| gradient: isUser | ||
| ? LinearGradient( | ||
| colors: [ | ||
| AppColors.userBubbleGradientStart, | ||
| AppColors.userBubbleGradientEnd, | ||
| ], | ||
| begin: Alignment.topLeft, | ||
| end: Alignment.bottomRight, | ||
| ) | ||
| : null, | ||
| color: isUser ? null : AppColors.assistantBubbleBg(context), | ||
| borderRadius: BorderRadius.only( | ||
| topLeft: const Radius.circular(radius), | ||
| topRight: const Radius.circular(radius), | ||
| bottomLeft: Radius.circular(isUser ? radius : 4), | ||
| bottomRight: Radius.circular(isUser ? 4 : radius), | ||
| ), | ||
| const SizedBox(height: 6), | ||
| Container( | ||
| padding: const EdgeInsets.all(AppSpacing.mediumLarge), | ||
| decoration: BoxDecoration( | ||
| color: isUser | ||
| ? AppColors.backgroundGray5(context) | ||
| : AppColors.primaryBlue.withValues(alpha: 0.08), | ||
| borderRadius: BorderRadius.circular( | ||
| AppSpacing.cornerRadiusBubble, | ||
| ), | ||
| ), | ||
| child: Text(turn.text, style: AppTypography.body(context)), | ||
| ), | ||
| child: Text( | ||
| turn.text, | ||
| style: AppTypography.body(context).copyWith( | ||
| color: isUser ? Colors.white : AppColors.textPrimary(context), | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| return Padding( | ||
| padding: const EdgeInsets.only(bottom: AppSpacing.mediumLarge), | ||
| child: Row( | ||
| mainAxisAlignment: | ||
| isUser ? MainAxisAlignment.end : MainAxisAlignment.start, | ||
| children: [bubble], |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Preserve speaker roles for assistive tech.
The new bubble UI removes the speaker label, leaving user/assistant identity conveyed only by alignment and color. Add semantics so screen readers still announce the role.
♿ Proposed fix
- return Padding(
- padding: const EdgeInsets.only(bottom: AppSpacing.mediumLarge),
- child: Row(
- mainAxisAlignment:
- isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
- children: [bubble],
+ return Semantics(
+ label: '${isUser ? 'You' : 'Assistant'}: ${turn.text}',
+ child: ExcludeSemantics(
+ child: Padding(
+ padding: const EdgeInsets.only(bottom: AppSpacing.mediumLarge),
+ child: Row(
+ mainAxisAlignment:
+ isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
+ children: [bubble],
+ ),
+ ),
),
);📝 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.
| const radius = AppSpacing.cornerRadiusBubble; | |
| return Padding( | |
| padding: const EdgeInsets.only(bottom: AppSpacing.large), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text( | |
| speaker, | |
| style: AppTypography.caption(context).copyWith( | |
| color: AppColors.textSecondary(context), | |
| fontWeight: FontWeight.w500, | |
| ), | |
| final bubble = ConstrainedBox( | |
| constraints: BoxConstraints( | |
| maxWidth: MediaQuery.of(context).size.width * 0.76, | |
| ), | |
| child: Container( | |
| padding: const EdgeInsets.symmetric( | |
| horizontal: AppSpacing.large, | |
| vertical: AppSpacing.mediumLarge, | |
| ), | |
| decoration: BoxDecoration( | |
| gradient: isUser | |
| ? LinearGradient( | |
| colors: [ | |
| AppColors.userBubbleGradientStart, | |
| AppColors.userBubbleGradientEnd, | |
| ], | |
| begin: Alignment.topLeft, | |
| end: Alignment.bottomRight, | |
| ) | |
| : null, | |
| color: isUser ? null : AppColors.assistantBubbleBg(context), | |
| borderRadius: BorderRadius.only( | |
| topLeft: const Radius.circular(radius), | |
| topRight: const Radius.circular(radius), | |
| bottomLeft: Radius.circular(isUser ? radius : 4), | |
| bottomRight: Radius.circular(isUser ? 4 : radius), | |
| ), | |
| const SizedBox(height: 6), | |
| Container( | |
| padding: const EdgeInsets.all(AppSpacing.mediumLarge), | |
| decoration: BoxDecoration( | |
| color: isUser | |
| ? AppColors.backgroundGray5(context) | |
| : AppColors.primaryBlue.withValues(alpha: 0.08), | |
| borderRadius: BorderRadius.circular( | |
| AppSpacing.cornerRadiusBubble, | |
| ), | |
| ), | |
| child: Text(turn.text, style: AppTypography.body(context)), | |
| ), | |
| child: Text( | |
| turn.text, | |
| style: AppTypography.body(context).copyWith( | |
| color: isUser ? Colors.white : AppColors.textPrimary(context), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| return Padding( | |
| padding: const EdgeInsets.only(bottom: AppSpacing.mediumLarge), | |
| child: Row( | |
| mainAxisAlignment: | |
| isUser ? MainAxisAlignment.end : MainAxisAlignment.start, | |
| children: [bubble], | |
| const radius = AppSpacing.cornerRadiusBubble; | |
| final bubble = ConstrainedBox( | |
| constraints: BoxConstraints( | |
| maxWidth: MediaQuery.of(context).size.width * 0.76, | |
| ), | |
| child: Container( | |
| padding: const EdgeInsets.symmetric( | |
| horizontal: AppSpacing.large, | |
| vertical: AppSpacing.mediumLarge, | |
| ), | |
| decoration: BoxDecoration( | |
| gradient: isUser | |
| ? LinearGradient( | |
| colors: [ | |
| AppColors.userBubbleGradientStart, | |
| AppColors.userBubbleGradientEnd, | |
| ], | |
| begin: Alignment.topLeft, | |
| end: Alignment.bottomRight, | |
| ) | |
| : null, | |
| color: isUser ? null : AppColors.assistantBubbleBg(context), | |
| borderRadius: BorderRadius.only( | |
| topLeft: const Radius.circular(radius), | |
| topRight: const Radius.circular(radius), | |
| bottomLeft: Radius.circular(isUser ? radius : 4), | |
| bottomRight: Radius.circular(isUser ? 4 : radius), | |
| ), | |
| ), | |
| child: Text( | |
| turn.text, | |
| style: AppTypography.body(context).copyWith( | |
| color: isUser ? Colors.white : AppColors.textPrimary(context), | |
| ), | |
| ), | |
| ), | |
| ); | |
| return Semantics( | |
| label: '${isUser ? 'You' : 'Assistant'}: ${turn.text}', | |
| child: ExcludeSemantics( | |
| child: Padding( | |
| padding: const EdgeInsets.only(bottom: AppSpacing.mediumLarge), | |
| child: Row( | |
| mainAxisAlignment: | |
| isUser ? MainAxisAlignment.end : MainAxisAlignment.start, | |
| children: [bubble], | |
| ), | |
| ), | |
| ), | |
| ); |
🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart`
around lines 584 - 628, The bubble UI in the voice_assistant_view.dart file
lacks semantic information for screen readers to announce the speaker role. Wrap
the Text widget displaying turn.text with a Semantics widget that includes a
label parameter to announce the speaker identity, such as "User message" or
"Assistant message" based on the isUser variable, ensuring that assistive
technology users can understand who is speaking even without visual color and
alignment cues.
| const cleanupVoiceSession = useCallback(async () => { | ||
| if (unsubscribeRef.current) { | ||
| unsubscribeRef.current(); | ||
| unsubscribeRef.current = null; | ||
| if (micDriverRef.current) { | ||
| micDriverRef.current.stop(); | ||
| micDriverRef.current = null; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Clear the delayed error reset during cleanup.
Line 191 can set the screen back to Listening after the user stops the session or the component unmounts. Store the timeout and clear it in cleanupVoiceSession; only reset if a driver is still active.
Suggested fix
const micDriverRef = useRef<VoiceAgentMicDriver | null>(null);
const scrollRef = useRef<ScrollView>(null);
+ const errorResetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const allModelsLoaded = sttModel && llmModel && ttsModel && vadModel;
const cleanupVoiceSession = useCallback(async () => {
+ if (errorResetTimerRef.current) {
+ clearTimeout(errorResetTimerRef.current);
+ errorResetTimerRef.current = null;
+ }
if (micDriverRef.current) {
micDriverRef.current.stop();
micDriverRef.current = null;
@@
onError: (err) => {
console.error('[VoiceAssistant] Voice turn error:', err);
setStatus(VoicePipelineStatus.Error);
- setTimeout(() => setStatus(VoicePipelineStatus.Listening), 2000);
+ if (errorResetTimerRef.current) {
+ clearTimeout(errorResetTimerRef.current);
+ }
+ errorResetTimerRef.current = setTimeout(() => {
+ if (micDriverRef.current) {
+ setStatus(VoicePipelineStatus.Listening);
+ }
+ errorResetTimerRef.current = null;
+ }, 2000);
},Also applies to: 188-191
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`
around lines 59 - 62, Store the timeout ID from the delayed error reset (around
lines 188-191) in a ref, and then clear that timeout in the cleanupVoiceSession
function before setting micDriverRef.current to null. This prevents the timeout
from firing after cleanup and resetting the screen state to "Listening" when the
session has already stopped or the component has unmounted.



v2 SDK testing-pass fixes across telemetry, Flutter, React Native, and Web.
Telemetry (commons + per-SDK)
Flutter
React Native
Web
rac_auth_get_access_tokenfor WASM telemetry auth.models.hydratedUI refresh), live download progress (asynchttp_downloadslot + event-driven C++ driver), VLM pthread-pool deadlock, RAG + Vision download UI, prebuilt ONNX/sherpa WASM vendoring.Known follow-ups (not in this branch)
Note
Medium Risk
Touches SDK environment and backend auth across three platforms; the React Native example commits a default production-style API key in source, which is a credential exposure risk for a public repo.
Overview
Aligns Android, Flutter, and React Native example apps with staging
RunAnywhere.initializeso local/dev Railway base URLs and telemetry/logging work (production was ignoring custom URLs and silencing logs).Flutter adds
--dart-definedefaults for API key/URL, lazy tab mounting so hidden tabs don’t spin up view-models, and guards against model-lifecycle feedback loops (chat + voice) that could ANR. RAG/VLM/voice get pipeline/camera/dispose cleanup fixes; Android Gradle/Kotlin bumps and small launcher/status-bar polish.React Native is a large v2 example redesign: flat
RootNavigator(3 tabs + feature routes), themed IntroScreen, redesigned chat composer/messages/model picker on action-sheet bottom sheets, reanimated keyboard padding, Figtree fonts, and Genie removed from the example Android build path. Bootstrap now falls back to in-repo default Railway API key/URL when Settings has no custom config (staging init). Android example pins kotlinx-coroutines for AAR compatibility.Reviewed by Cursor Bugbot for commit 2bbba92. Configure here.
Summary by CodeRabbit
New Features
Bug Fixes
Chores