Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9d184e1
feat(pro): autolink the pro native library via react-native.config.js
dishit-wednesday Jun 27, 2026
dca0b1a
feat(whisper): file-transcription support (chunkable opts, stop, nati…
dishit-wednesday Jun 27, 2026
6041359
feat(whisper): add whisper remote-server provider type
dishit-wednesday Jun 27, 2026
4571d4f
feat(core): persistent + in-memory debug logging for export
dishit-wednesday Jun 27, 2026
5af7bbe
feat(ios): background-audio mode for locket recorder
dishit-wednesday Jun 27, 2026
6a838e0
chore(ios): pod install for OffgridPro
dishit-wednesday Jun 27, 2026
44d8694
chore: bump pro submodule to feat/locket (locket feature)
dishit-wednesday Jun 27, 2026
684fce5
feat(transcription): offset/duration chunking, live segments, CoreML …
dishit-wednesday Jun 28, 2026
93c3be9
feat(nav): Memory tab (Pro recorder/paywall) + move Settings to Home …
dishit-wednesday Jun 28, 2026
58b5656
chore: bump pro submodule (transcription slicing + attach-to-chat)
dishit-wednesday Jun 28, 2026
7395249
chore: bump pro submodule (meeting intelligence + transcription work)
dishit-wednesday Jun 29, 2026
a7aea86
fix(whisper): iOS streaming lifetime patch + PATCH-LIVE marker (whisp…
dishit-wednesday Jun 29, 2026
2bc1aa9
feat(whisper): CoreML asset guard, force English for .en models, tdrz…
dishit-wednesday Jun 29, 2026
d91a962
chore: on-device memory-snapshot logging util
dishit-wednesday Jun 29, 2026
39cf93a
feat(whisper): log memory footprint around model load
dishit-wednesday Jun 29, 2026
863981f
feat(nav): rename Memory tab to Recorder
dishit-wednesday Jun 29, 2026
dc7a08a
test(whisper): assert loadModel called with options arg
dishit-wednesday Jun 29, 2026
62bd573
polish(recorder): redesign the Recorder paywall
dishit-wednesday Jun 29, 2026
5164294
chore: bump pro submodule (recorder + meeting intelligence + UI polish)
dishit-wednesday Jun 29, 2026
bf4ea87
chore(whisper): defer transcribe-service refactor (eslint max-lines/c…
dishit-wednesday Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions __tests__/unit/stores/whisperStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ describe('whisperStore', () => {
expect(mockWhisperService.getModelPath).toHaveBeenCalledWith('ggml-tiny');
expect(mockWhisperService.loadModel).toHaveBeenCalledWith(
'/models/ggml-tiny',
undefined,
);
expect(getState().isModelLoaded).toBe(true);
});
Expand Down Expand Up @@ -252,6 +253,7 @@ describe('whisperStore', () => {
expect(mockWhisperService.getModelPath).toHaveBeenCalledWith('ggml-tiny');
expect(mockWhisperService.loadModel).toHaveBeenCalledWith(
'/models/ggml-tiny',
undefined,
);
expect(getState().isModelLoaded).toBe(true);
expect(getState().isModelLoading).toBe(false);
Expand Down
68 changes: 68 additions & 0 deletions __tests__/unit/utils/memorySnapshot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* memorySnapshot unit tests
*
* logMemory() is a diagnostics probe used around whisper model load and each
* transcribe chunk to capture the app's footprint. On iOS it surfaces whether
* an apparent transcription "crash" was actually a jetsam low-memory kill.
*
* Guarantees under test:
* - formats used/total in MB and a percentage
* - never throws (a failing probe must not break the path it observes)
* - no divide-by-zero when total memory is reported as 0
*/

import DeviceInfo from 'react-native-device-info';
import logger from '../../../src/utils/logger';
import { logMemory } from '../../../src/utils/memorySnapshot';

const mockedDeviceInfo = DeviceInfo as jest.Mocked<typeof DeviceInfo>;

describe('logMemory', () => {
let logSpy: jest.SpyInstance;
let warnSpy: jest.SpyInstance;

beforeEach(() => {
jest.clearAllMocks();
logSpy = jest.spyOn(logger, 'log').mockImplementation(() => {});
warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {});
});

afterEach(() => {
logSpy.mockRestore();
warnSpy.mockRestore();
});

it('logs used/total in MB with a percentage, tagged with the call site', async () => {
mockedDeviceInfo.getUsedMemory.mockResolvedValue(1.4 * 1024 * 1024 * 1024);
mockedDeviceInfo.getTotalMemory.mockResolvedValue(4 * 1024 * 1024 * 1024);

await logMemory('whisper:beforeLoad');

expect(logSpy).toHaveBeenCalledTimes(1);
const msg = logSpy.mock.calls[0][0] as string;
expect(msg).toContain('[mem] whisper:beforeLoad');
expect(msg).toContain('used=1434MB');
expect(msg).toContain('total=4096MB');
expect(msg).toContain('(35%)');
});

it('never throws and warns when the probe fails', async () => {
mockedDeviceInfo.getUsedMemory.mockRejectedValue(new Error('boom'));
mockedDeviceInfo.getTotalMemory.mockResolvedValue(4 * 1024 * 1024 * 1024);

await expect(logMemory('transcribe:chunk@0s')).resolves.toBeUndefined();
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('snapshot failed'));
expect(logSpy).not.toHaveBeenCalled();
});

it('does not divide by zero when total memory is unavailable', async () => {
mockedDeviceInfo.getUsedMemory.mockResolvedValue(100 * 1024 * 1024);
mockedDeviceInfo.getTotalMemory.mockResolvedValue(0);

await logMemory('zero');

const msg = logSpy.mock.calls[0][0] as string;
expect(msg).toContain('total=0MB');
expect(msg).toContain('(0%)');
});
});
24 changes: 14 additions & 10 deletions ios/OffgridMobile/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand All @@ -29,19 +33,15 @@
</array>
</dict>
</array>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>twitter</string>
<string>x</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
Expand All @@ -53,6 +53,10 @@
<string>_ollama._tcp</string>
<string>_lmstudio._tcp</string>
</array>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>Used to read and create calendar events on your request.</string>
<key>NSCalendarsUsageDescription</key>
<string>Used to read and create calendar events on your request.</string>
<key>NSCameraUsageDescription</key>
<string>This app needs access to your camera to take photos and attach them to conversations.</string>
<key>NSFaceIDUsageDescription</key>
Expand All @@ -65,10 +69,6 @@
<string>This app needs permission to save generated images to your photo library.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photo library to attach images to conversations.</string>
<key>NSCalendarsUsageDescription</key>
<string>Used to read and create calendar events on your request.</string>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>Used to read and create calendar events on your request.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app uses on-device speech recognition to transcribe voice input.</string>
<key>RCTNewArchEnabled</key>
Expand All @@ -95,6 +95,10 @@
<string>SimpleLineIcons.ttf</string>
<string>FontAwesome6_Brands.ttf</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
Expand Down
36 changes: 34 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,34 @@ PODS:
- MMKV (2.4.0):
- MMKVCore (~> 2.4.0)
- MMKVCore (2.4.0)
- OffgridPro (0.0.1):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- op-sqlite (15.2.5):
- boost
- DoubleConversion
Expand Down Expand Up @@ -3570,6 +3598,7 @@ DEPENDENCIES:
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- llama-rn (from `../node_modules/llama.rn`)
- lottie-react-native (from `../node_modules/lottie-react-native`)
- OffgridPro (from `../pro/ios`)
- "op-sqlite (from `../node_modules/@op-engineering/op-sqlite`)"
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
Expand Down Expand Up @@ -3708,6 +3737,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/llama.rn"
lottie-react-native:
:path: "../node_modules/lottie-react-native"
OffgridPro:
:path: "../pro/ios"
op-sqlite:
:path: "../node_modules/@op-engineering/op-sqlite"
RCT-Folly:
Expand Down Expand Up @@ -3917,12 +3948,13 @@ SPEC CHECKSUMS:
FBLazyVector: 309703e71d3f2f1ed7dc7889d58309c9d77a95a4
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: 8a072b98dfc36920dbf6eb10468ab5520f2c4b37
hermes-engine: 8c6be38f94b3bf8b864981980e64e55f08e467ec
llama-rn: 7c9b9610cc259118508bd1264bbd9be47e17ffd0
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
lottie-react-native: 691b8363e8c591fb78a78254ff2517258891456b
MMKV: 86859fdfa2b0b21db1fd6e48788474a6416a2c77
MMKVCore: 3d16ce9f7d411e135020915fde98a056859a1efa
OffgridPro: bd17e59d526e0cd255c29d92dddbfa3a5796b142
op-sqlite: bafff369cecaee4fe65c89eec47deaba26f2db95
opencv-rne: 2305807573b6e29c8c87e3416ab096d09047a7a0
PurchasesHybridCommon: eed735a411c1aee8c05d62933fa7c4a40ede4009
Expand Down Expand Up @@ -3967,7 +3999,7 @@ SPEC CHECKSUMS:
react-native-blur: 6af83e7e3c4c1446a188d9b2c493600fc4beb173
react-native-document-picker: dc2d83366e47e89e7c51e8a41eab99c1d54e941c
react-native-document-viewer: 8c6ed07e7e27352743fa98e8dd6d288ad925b884
react-native-executorch: 992a2b4ce98cbf47e4b7904d6680c0be379f7ec1
react-native-executorch: 65df20362342afff0040d227d270a1b9a59f0c54
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-image-picker: 0314366753615115fa55c3cc937ac44cb7e75702
react-native-keyboard-controller: 7534b5a39d1e8b2b79f86e8e998ed71c7154f69f
Expand Down
44 changes: 44 additions & 0 deletions patches/whisper.rn+0.5.5.patch
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,47 @@ index b1fa548..5f0b7f1 100644
if (job->audio_output_path != nullptr) {
RNWHISPER_LOG_INFO("job->params.language: %s\n", job->params.language);
std::vector<int> slice_n_samples_vec;
diff --git a/node_modules/whisper.rn/ios/RNWhisperContext.mm b/node_modules/whisper.rn/ios/RNWhisperContext.mm
index 13a880f..4ccf878 100644
--- a/node_modules/whisper.rn/ios/RNWhisperContext.mm
+++ b/node_modules/whisper.rn/ios/RNWhisperContext.mm
@@ -419,6 +419,24 @@ - (void)transcribeData:(int)jobId

whisper_full_params params = [self createParams:options jobId:jobId];

+ // Hoisted to the dispatch-block scope so it outlives the `if` below and
+ // stays valid through fullTranscribe. It was declared inside the
+ // `if (onNewSegments)` block but its address is handed to whisper as the
+ // segment-callback context; once that `if` closed, the stack struct was
+ // dead, and the first segment callback (~first 30s window) dereferenced a
+ // dangling pointer -> deterministic native crash on iOS. (onProgress was
+ // unaffected: it passes the block directly, no struct.)
+ struct rnwhisper_segments_callback_data user_data = {
+ .onNewSegments = onNewSegments,
+ .tdrzEnable = options[@"tdrzEnable"] && [options[@"tdrzEnable"] boolValue],
+ .total_n_new = 0,
+ };
+ // Marker proving this binary carries the whisper.rn+0.5.5 segment-callback
+ // lifetime patch. If you DON'T see this line in the device log when iOS
+ // streaming is enabled, the running app was built without the patch (likely
+ // a Metro reload over a stale binary) and the live callback will crash.
+ NSLog(@"[RNWhisper][PATCH-LIVE] segment-callback user_data hoisted (whisper.rn+0.5.5 lifetime patch compiled in)");
+
if (options[@"onProgress"] && [options[@"onProgress"] boolValue]) {
params.progress_callback = [](struct whisper_context * /*ctx*/, struct whisper_state * /*state*/, int progress, void * user_data) {
void (^onProgress)(int) = (__bridge void (^)(int))user_data;
@@ -463,11 +481,9 @@ - (void)transcribeData:(int)jobId
void (^onNewSegments)(NSDictionary *) = (void (^)(NSDictionary *))data->onNewSegments;
onNewSegments(result);
};
- struct rnwhisper_segments_callback_data user_data = {
- .onNewSegments = onNewSegments,
- .tdrzEnable = options[@"tdrzEnable"] && [options[@"tdrzEnable"] boolValue],
- .total_n_new = 0,
- };
+ // user_data is declared at the dispatch-block scope above so it stays
+ // alive through fullTranscribe (declaring it here, inside the if, left a
+ // dangling pointer once this block closed).
params.new_segment_callback_user_data = &user_data;
}

2 changes: 1 addition & 1 deletion pro
Submodule pro updated from d32d94 to bb2381
36 changes: 36 additions & 0 deletions react-native.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const fs = require('fs');
const path = require('path');

// Autolink the pro submodule's native library ONLY when it is actually on
// disk. Mirrors the fs.existsSync(pro) guard metro.config.js uses for the pro
// JS: a public clone without the private submodule sees an empty/absent pro/
// dir, this entry is omitted, and the open build compiles with no pro native.
//
// IMPORTANT: check a real file inside pro/, never just the pro/ directory - an
// uninitialised submodule leaves an empty pro/ folder behind.
const proRoot = path.resolve(__dirname, 'pro');
const proAndroidGradle = path.join(proRoot, 'android', 'build.gradle');
const proPodspec = path.join(proRoot, 'ios', 'OffgridPro.podspec');
const proHasNative = fs.existsSync(proAndroidGradle);

module.exports = {
dependencies: {
...(proHasNative
? {
'@offgrid/pro': {
root: proRoot,
platforms: {
android: {
sourceDir: path.join(proRoot, 'android'),
packageImportPath: 'import ai.offgridmobile.alwayson.AlwaysOnTranscriptionPackage;',
packageInstance: 'new AlwaysOnTranscriptionPackage()',
},
ios: {
podspecPath: proPodspec,
},
},
},
}
: {}),
Comment on lines +14 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the iOS autolink with the podspec as well.

Line 14 uses pro/android/build.gradle to enable both platform entries, so a checkout with Android native files present but pro/ios/OffgridPro.podspec missing will still hand CocoaPods a dead podspecPath and break pod install. Split the guard per platform, or at least include the podspec in the condition.

Suggested fix
 const proRoot = path.resolve(__dirname, 'pro');
 const proAndroidGradle = path.join(proRoot, 'android', 'build.gradle');
 const proPodspec = path.join(proRoot, 'ios', 'OffgridPro.podspec');
-const proHasNative = fs.existsSync(proAndroidGradle);
+const proHasAndroidNative = fs.existsSync(proAndroidGradle);
+const proHasIosNative = fs.existsSync(proPodspec);

 module.exports = {
   dependencies: {
-    ...(proHasNative
+    ...(proHasAndroidNative || proHasIosNative
       ? {
           '`@offgrid/pro`': {
             root: proRoot,
             platforms: {
-              android: {
-                sourceDir: path.join(proRoot, 'android'),
-                packageImportPath: 'import ai.offgridmobile.alwayson.AlwaysOnTranscriptionPackage;',
-                packageInstance: 'new AlwaysOnTranscriptionPackage()',
-              },
-              ios: {
-                podspecPath: proPodspec,
-              },
+              ...(proHasAndroidNative
+                ? {
+                    android: {
+                      sourceDir: path.join(proRoot, 'android'),
+                      packageImportPath: 'import ai.offgridmobile.alwayson.AlwaysOnTranscriptionPackage;',
+                      packageInstance: 'new AlwaysOnTranscriptionPackage()',
+                    },
+                  }
+                : {}),
+              ...(proHasIosNative
+                ? {
+                    ios: {
+                      podspecPath: proPodspec,
+                    },
+                  }
+                : {}),
             },
           },
         }
📝 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.

Suggested change
const proHasNative = fs.existsSync(proAndroidGradle);
module.exports = {
dependencies: {
...(proHasNative
? {
'@offgrid/pro': {
root: proRoot,
platforms: {
android: {
sourceDir: path.join(proRoot, 'android'),
packageImportPath: 'import ai.offgridmobile.alwayson.AlwaysOnTranscriptionPackage;',
packageInstance: 'new AlwaysOnTranscriptionPackage()',
},
ios: {
podspecPath: proPodspec,
},
},
},
}
: {}),
const proRoot = path.resolve(__dirname, 'pro');
const proAndroidGradle = path.join(proRoot, 'android', 'build.gradle');
const proPodspec = path.join(proRoot, 'ios', 'OffgridPro.podspec');
const proHasAndroidNative = fs.existsSync(proAndroidGradle);
const proHasIosNative = fs.existsSync(proPodspec);
module.exports = {
dependencies: {
...(proHasAndroidNative || proHasIosNative
? {
'`@offgrid/pro`': {
root: proRoot,
platforms: {
...(proHasAndroidNative
? {
android: {
sourceDir: path.join(proRoot, 'android'),
packageImportPath: 'import ai.offgridmobile.alwayson.AlwaysOnTranscriptionPackage;',
packageInstance: 'new AlwaysOnTranscriptionPackage()',
},
}
: {}),
...(proHasIosNative
? {
ios: {
podspecPath: proPodspec,
},
}
: {}),
},
},
}
: {}),
🤖 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 `@react-native.config.js` around lines 14 - 34, The autolink guard in
react-native.config.js currently uses only pro/android/build.gradle to enable
both Android and iOS entries, which can still expose a missing podspecPath to
CocoaPods. Update the module-level config around proHasNative and module.exports
so the iOS autolink under '`@offgrid/pro`' is only included when the podspec
exists, either by splitting the platform guards or by extending the existing
condition to check proPodspec before setting the ios podspecPath.

},
};
2 changes: 1 addition & 1 deletion src/components/onboarding/spotlightConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const STEP_TAB_MAP: Record<string, string> = {
downloadedModel: 'ModelsTab',
loadedModel: 'HomeTab',
sentMessage: 'ChatsTab',
exploredSettings: 'SettingsTab',
exploredSettings: 'Settings',
createdProject: 'ProjectsTab',
triedImageGen: 'ModelsTab',
};
Expand Down
10 changes: 6 additions & 4 deletions src/navigation/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
MainTabParamList,
} from './types';
import { useRegisteredScreens } from './screenRegistry';
import { MemoryTabScreen } from '../screens/MemoryTabScreen';

const RootStack = createNativeStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator<MainTabParamList>();
Expand All @@ -55,7 +56,7 @@ const TAB_ICON_MAP: Record<string, string> = {
ChatsTab: 'message-circle',
ProjectsTab: 'folder',
ModelsTab: 'cpu',
SettingsTab: 'settings',
MemoryTab: 'mic',
};

const TabBarIcon: React.FC<{ name: string; focused: boolean }> = ({ name, focused }) => {
Expand Down Expand Up @@ -173,9 +174,9 @@ const MainTabs: React.FC = () => {
})}
/>
<Tab.Screen
name="SettingsTab"
component={SettingsScreen}
options={{ tabBarLabel: 'Settings', tabBarButtonTestID: 'settings-tab' }}
name="MemoryTab"
component={MemoryTabScreen}
options={{ tabBarLabel: 'Recorder', tabBarButtonTestID: 'recorder-tab' }}
listeners={() => ({
tabPress: () => { triggerHaptic('selection'); },
})}
Expand Down Expand Up @@ -232,6 +233,7 @@ export const AppNavigator: React.FC = () => {
/>
<RootStack.Screen name="KnowledgeBase" component={KnowledgeBaseScreen} />
<RootStack.Screen name="DocumentPreview" component={DocumentPreviewScreen} />
<RootStack.Screen name="Settings" component={SettingsScreen} />
<RootStack.Screen name="ModelSettings" component={ModelSettingsScreen} />
<RootStack.Screen name="RemoteServers" component={RemoteServersScreen} />
<RootStack.Screen name="DeviceInfo" component={DeviceInfoScreen} />
Expand Down
3 changes: 2 additions & 1 deletion src/navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type RootStackParamList = {
KnowledgeBase: { projectId: string };
DocumentPreview: { filePath: string; fileName: string; fileSize: number };
// Former SettingsStack
Settings: undefined;
ModelSettings: undefined;
RemoteServers: undefined;
DeviceInfo: undefined;
Expand All @@ -32,5 +33,5 @@ export type MainTabParamList = {
ChatsTab: undefined;
ProjectsTab: undefined;
ModelsTab: { initialTab?: 'text' | 'image' | 'voice' | 'transcription'; repairModelId?: string } | undefined;
SettingsTab: undefined;
MemoryTab: undefined;
};
11 changes: 8 additions & 3 deletions src/screens/HomeScreen/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,14 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ navigation }) => {
<Text style={styles.title}>Off Grid</Text>
{showIcon && <PulsatingIcon onPress={openSheet} />}
</View>
<TouchableOpacity onPress={() => navigation.navigate('ProDetail')} hitSlop={8} style={styles.crownButton}>
<IconMC name="crown" size={16} color={colors.primary} />
</TouchableOpacity>
<View style={styles.headerRight}>
<TouchableOpacity onPress={() => navigation.navigate('Settings')} hitSlop={8} style={styles.iconButton}>
<Icon name="settings" size={18} color={colors.textSecondary} />
</TouchableOpacity>
<TouchableOpacity onPress={() => navigation.navigate('ProDetail')} hitSlop={8} style={styles.crownButton}>
<IconMC name="crown" size={16} color={colors.primary} />
</TouchableOpacity>
</View>
Comment on lines +137 to +144

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add accessibility labels to the new icon-only header actions.

These buttons are icon-only, so screen-reader users won’t get a reliable action name here without explicit labels.

Suggested fix
-              <TouchableOpacity onPress={() => navigation.navigate('Settings')} hitSlop={8} style={styles.iconButton}>
+              <TouchableOpacity
+                accessibilityRole="button"
+                accessibilityLabel="Open settings"
+                onPress={() => navigation.navigate('Settings')}
+                hitSlop={8}
+                style={styles.iconButton}
+              >
                 <Icon name="settings" size={18} color={colors.textSecondary} />
               </TouchableOpacity>
-              <TouchableOpacity onPress={() => navigation.navigate('ProDetail')} hitSlop={8} style={styles.crownButton}>
+              <TouchableOpacity
+                accessibilityRole="button"
+                accessibilityLabel="Open Pro details"
+                onPress={() => navigation.navigate('ProDetail')}
+                hitSlop={8}
+                style={styles.crownButton}
+              >
                 <IconMC name="crown" size={16} color={colors.primary} />
               </TouchableOpacity>
📝 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.

Suggested change
<View style={styles.headerRight}>
<TouchableOpacity onPress={() => navigation.navigate('Settings')} hitSlop={8} style={styles.iconButton}>
<Icon name="settings" size={18} color={colors.textSecondary} />
</TouchableOpacity>
<TouchableOpacity onPress={() => navigation.navigate('ProDetail')} hitSlop={8} style={styles.crownButton}>
<IconMC name="crown" size={16} color={colors.primary} />
</TouchableOpacity>
</View>
<View style={styles.headerRight}>
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel="Open settings"
onPress={() => navigation.navigate('Settings')}
hitSlop={8}
style={styles.iconButton}
>
<Icon name="settings" size={18} color={colors.textSecondary} />
</TouchableOpacity>
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel="Open Pro details"
onPress={() => navigation.navigate('ProDetail')}
hitSlop={8}
style={styles.crownButton}
>
<IconMC name="crown" size={16} color={colors.primary} />
</TouchableOpacity>
</View>
🤖 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 `@src/screens/HomeScreen/index.tsx` around lines 137 - 144, Add explicit
accessibility labels to the icon-only header actions in HomeScreen’s headerRight
so screen readers can identify them. Update the TouchableOpacity controls that
navigate to Settings and ProDetail to include clear accessibilityLabel (and, if
needed, accessibilityRole) values that describe each action, using the existing
navigation handlers and the headerRight section to locate them.

</View>

{/* Collapsed Models summary — tap to open the manager sheet. Both the
Expand Down
Loading
Loading