Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
0459314
fix(S3358): extract nested ternary into getStatusText helper
alichherawalla Feb 20, 2026
c9737b4
fix(S6582): use optional chain for metadata filename check
alichherawalla Feb 20, 2026
6097582
fix(S2486): handle caught exceptions with error logging
alichherawalla Feb 20, 2026
1512d93
fix(S7773): prefer Number.isNaN over global isNaN
alichherawalla Feb 20, 2026
bd9776d
fix(S6439): convert quantization string conditional to boolean
alichherawalla Feb 20, 2026
28f734f
fix(S6479): use stable model identifiers as list keys
alichherawalla Feb 20, 2026
c7265ba
fix(S6594/S6397): use RegExp.exec and simplify regex character class
alichherawalla Feb 20, 2026
c6ada17
fix(S6544/S2004): extract async logic from alert onPress callbacks
alichherawalla Feb 20, 2026
ab0cbb1
test(DownloadManagerScreen): add getStatusText status display coverage
alichherawalla Feb 20, 2026
9bb49c1
fix(S7748): remove trailing zero fractions from memory threshold cons…
alichherawalla Feb 20, 2026
8f1fe91
test(ios): add XCTest unit-test infrastructure for native Swift modules
alichherawalla Feb 20, 2026
10df4b3
fix(S2933): mark listeners and loadingState fields as readonly
alichherawalla Feb 20, 2026
8f5e227
chore(ci): move iOS unit tests into ci.yml, remove test-ios.yml
alichherawalla Feb 20, 2026
630f7e8
fix(S2094): reduce cognitive complexity of DownloadCompleteBroadcastR…
alichherawalla Feb 20, 2026
60210eb
chore: add test:android and test:ios to quality gate in CLAUDE.md
alichherawalla Feb 20, 2026
ebc8ac0
ci: add test-android job to CI workflow
alichherawalla Feb 20, 2026
2010a9e
fix(S3776): reduce checkMemoryForModel cognitive complexity from 18 t…
alichherawalla Feb 20, 2026
24e45e4
test(ios): expand XCTest suite from 12 to 30 cases across all 3 modules
alichherawalla Feb 20, 2026
ed2e5eb
fix(S7748): remove zero fraction from default guidanceScale in ChatSc…
alichherawalla Feb 20, 2026
9d9f9c6
fix(S3923): remove identical-branch ternary for keyboardVerticalOffset
alichherawalla Feb 20, 2026
99c0097
fix(S6754): destructure useState into setter only when value is unused
alichherawalla Feb 20, 2026
93bf784
fix(S6582): use optional chain for classifierModel filePath check
alichherawalla Feb 20, 2026
4208f7b
fix(S7755): use Array.at(-1) instead of bracket length-index access
alichherawalla Feb 20, 2026
4a8532f
fix(S1874): replace deprecated String.substr with String.slice
alichherawalla Feb 20, 2026
7433f33
fix(S2486/S7718): handle stop-generation catch block
alichherawalla Feb 20, 2026
4e8435f
fix(S2004): pass resolve directly to setTimeout
alichherawalla Feb 20, 2026
9466d5d
test(android): add unit tests for DownloadManagerModule and LocalDrea…
alichherawalla Feb 20, 2026
68890b6
fix(S6544): extract async delete-conversation handler
alichherawalla Feb 20, 2026
daec006
fix(security): address Gemini review comments on DownloadManagerScreen
alichherawalla Feb 20, 2026
36241bc
fix(S3358): extract nested ternaries in ChatScreen
alichherawalla Feb 20, 2026
a7d75d4
fix(S3776): reduce ensureModelLoaded cognitive complexity
alichherawalla Feb 20, 2026
9973e45
fix(S1874): replace absoluteFillObject with explicit style properties
alichherawalla Feb 20, 2026
6e91bef
fix(S6754): remove unused currentImageMode state
alichherawalla Feb 20, 2026
84b986b
fix(S2004): extract waitForRenderFrame to reduce function nesting depth
alichherawalla Feb 20, 2026
b8ab1b7
fix(S1874): replace deprecated InteractionManager.runAfterInteractions
alichherawalla Feb 20, 2026
c2818bf
test(android): fix DownloadManagerModuleTest to use Robolectric for J…
alichherawalla Feb 20, 2026
0ab8413
refactor(android): extract shouldRemoveDownload and saveRgbToPng to c…
alichherawalla Feb 20, 2026
ec62f26
test(android): add saveRgbToPng pixel-accuracy tests to LocalDreamMod…
alichherawalla Feb 20, 2026
daddc9b
chore(tooling): add Husky pre-commit hooks, expand ESLint rules, add …
alichherawalla Feb 20, 2026
506492c
fix(prefer-template): replace string concatenation with template lite…
alichherawalla Feb 20, 2026
fe6151c
chore(tooling): add Husky pre-commit hooks, lint-staged, and SwiftLint
alichherawalla Feb 20, 2026
9253793
test(ChatScreen): use fake timers in conversation-switch KV-cache test
alichherawalla Feb 20, 2026
fa95ff7
fix(lint): resolve no-shadow and no-empty violations in ChatScreen test
alichherawalla Feb 20, 2026
6b5dfb4
fix(prefer-template): auto-fix remaining string concatenations in tes…
alichherawalla Feb 20, 2026
01c763b
fix(S3776): reduce ChatScreen component cognitive complexity
alichherawalla Feb 20, 2026
3e18821
fix(no-shadow): remove shadowing React require in jest mock factories
alichherawalla Feb 20, 2026
7889ecd
Merge branch 'main' of github.com:alichherawalla/offline-mobile-llm-m…
alichherawalla Feb 20, 2026
4c2a148
chore: add .eslintignore for generated build artifacts
alichherawalla Feb 20, 2026
ff567b2
chore: sonar connection
alichherawalla Feb 20, 2026
f3c6f99
fix(ios): resolve SwiftLint violations in Swift native modules
alichherawalla Feb 21, 2026
0056c61
Merge branch 'main' of github.com:alichherawalla/offline-mobile-llm-m…
alichherawalla Feb 21, 2026
8f4385d
fix(lint): move inline styles to StyleSheet in Group 1 screens
alichherawalla Feb 21, 2026
bec11f9
refactor(ProjectDetailScreen): extract styles to separate file
alichherawalla Feb 21, 2026
1221077
fix(lint): eliminate inline styles in AppNavigator and AnimatedPressable
alichherawalla Feb 21, 2026
1b78dcc
refactor(max-params): decompose functions exceeding 3-parameter limit
alichherawalla Feb 21, 2026
187703d
test: update test suite to match max-params refactor
alichherawalla Feb 21, 2026
a5608b9
refactor(llm): split llm.ts into focused modules for Group 4 ESLint c…
alichherawalla Feb 21, 2026
29e6d17
refactor(llm): split llm.ts into focused modules for Group 4 ESLint c…
alichherawalla Feb 21, 2026
318e142
refactor: fix ESLint max-lines violations in ModelDownloadScreen, app…
alichherawalla Feb 21, 2026
fdf358f
refactor: fix ESLint violations in documentService, hardware, hugging…
alichherawalla Feb 21, 2026
3967f34
refactor: split modelManager.ts into src/services/modelManager/ folder
alichherawalla Feb 21, 2026
a885181
refactor: split activeModelService.ts into src/services/activeModelSe…
alichherawalla Feb 21, 2026
b899c5b
chore: remove old service files after folder-based splits and drop no…
alichherawalla Feb 21, 2026
953de92
refactor(AppSheet): extract styles and panResponder to fix ESLint max…
alichherawalla Feb 21, 2026
f3fbded
refactor(StorageSettingsScreen): extract styles and OrphanedFilesSect…
alichherawalla Feb 21, 2026
41ff635
refactor(ModelCard): extract styles and sub-components to fix ESLint …
alichherawalla Feb 21, 2026
f083c97
refactor(DownloadManagerScreen): extract into focused folder structure
alichherawalla Feb 21, 2026
bd5c9c9
refactor(ChatInput): split into focused companion files
alichherawalla Feb 21, 2026
898b001
fix(ChatInputVoice): use ref for onTranscript to prevent infinite re-…
alichherawalla Feb 21, 2026
1608b44
refactor(components): organize ChatInput, ModelSelectorModal, VoiceRe…
alichherawalla Feb 21, 2026
8551788
fix: clear attachments on send, fix inline styles in ModelSettingsScr…
alichherawalla Feb 21, 2026
83e24ac
fix: resolve remaining inline style violations in GenerationSettingsM…
alichherawalla Feb 21, 2026
1b363a3
refactor: split GalleryScreen into folder structure
alichherawalla Feb 21, 2026
e57c911
refactor: split ModelSettingsScreen into folder structure
alichherawalla Feb 21, 2026
1525985
refactor: split HomeScreen into folder structure
alichherawalla Feb 21, 2026
e0d60f2
refactor: split ChatMessage into folder structure
alichherawalla Feb 21, 2026
6e0054b
Merge branch 'worktree-agent-a0b61c46' into fix/sonar-download-manage…
alichherawalla Feb 21, 2026
2c82630
refactor: split HomeScreen into folder structure
alichherawalla Feb 21, 2026
a076fc0
Merge branch 'worktree-agent-ac0a105a' into fix/sonar-download-manage…
alichherawalla Feb 21, 2026
4f82d20
refactor: split GenerationSettingsModal and ChatScreen into folders
alichherawalla Feb 21, 2026
b86b16c
fix(ChatMessage): resolve ESLint violations after folder split
alichherawalla Feb 21, 2026
6c5b53c
refactor: split ModelsScreen into folder structure
alichherawalla Feb 21, 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
4 changes: 4 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Generated build artifacts
android/app/build/
ios/build/
coverage/
48 changes: 48 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
module.exports = {
root: true,
extends: '@react-native',
plugins: [
'react-native',
'react',
'react-hooks',
],
env: {
jest: true,
browser: true,
node: true,
es6: true,
},
rules: {
// TypeScript
'@typescript-eslint/no-unused-vars': [
'error',
{
Expand All @@ -10,5 +22,41 @@ module.exports = {
caughtErrorsIgnorePattern: '^_',
},
],
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',

// Code quality (built-in)
'no-empty': 'error',
'no-else-return': 'error',
'prefer-template': 'error',
complexity: ['error', 15],
'max-lines-per-function': ['error', 250],
'max-lines': ['error', 350],
'max-params': ['error', 3],
// React hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',

// React Native
'react-native/no-unused-styles': 'error',
'react-native/no-inline-styles': 'error',
'react-native/no-color-literals': 'error',
'react-native/no-raw-text': 'error',
'react-native/no-single-element-style-arrays': 'error',
},
overrides: [
{
// Relax structural rules in test files — large test suites and helpers are acceptable
files: ['__tests__/**/*', '*.test.ts', '*.test.tsx', 'jest.setup.ts'],
rules: {
'max-lines': 'off',
'max-lines-per-function': 'off',
'max-params': 'off',
complexity: 'off',
'react-native/no-inline-styles': 'off',
'react-native/no-raw-text': 'off',
'react-native/no-color-literals': 'off',
},
},
],
};
83 changes: 83 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,86 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
fail_ci_if_error: false

test-android:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install JS dependencies
run: npm ci

- name: Run Android unit tests
run: npm run test:android

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: android-test-results
path: android/app/build/reports/tests/
if-no-files-found: ignore

test-ios:
runs-on: macos-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install JS dependencies
run: npm ci

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true

- name: Install CocoaPods
run: |
gem install cocoapods
cd ios && pod install

- name: Install SwiftLint
run: brew install swiftlint

- name: Lint Swift
run: swiftlint lint --quiet --reporter github-actions-logging

- name: Run iOS unit tests
run: |
xcodebuild test \
-workspace ios/OffgridMobile.xcworkspace \
-scheme OffgridMobile \
-destination 'platform=iOS Simulator,name=iPhone 16e' \
-only-testing:OffgridMobileTests \
| xcpretty --color && exit "${PIPESTATUS[0]}"

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: ios-test-results
path: ~/Library/Developer/Xcode/DerivedData/**/Logs/Test/*.xcresult
if-no-files-found: ignore
43 changes: 43 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env sh

# Detect staged file types
STAGED_JS=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(ts|tsx|js|jsx)$' || true)
STAGED_SWIFT=$(git diff --cached --name-only --diff-filter=ACMR | grep '\.swift$' | grep -v 'Pods/' | grep -v 'build/' || true)
STAGED_KOTLIN=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(kt|kts)$' || true)

# ── JS / TS ────────────────────────────────────────────────────────────────────
if [ -n "$STAGED_JS" ]; then
echo "▶ JS/TS lint (staged files)..."
npx lint-staged

echo "▶ TypeScript type check..."
npx tsc --noEmit

echo "▶ JS/TS tests..."
npm test
fi

# ── Swift / iOS ────────────────────────────────────────────────────────────────
if [ -n "$STAGED_SWIFT" ]; then
if command -v swiftlint >/dev/null 2>&1; then
echo "▶ SwiftLint (staged files)..."
echo "$STAGED_SWIFT" | tr '\n' '\0' | xargs -0 swiftlint lint --quiet
else
echo "⚠️ SwiftLint not installed — skipping Swift lint. Install: brew install swiftlint"
fi

echo "▶ iOS tests..."
npm run test:ios
fi

# ── Kotlin / Android ───────────────────────────────────────────────────────────
if [ -n "$STAGED_KOTLIN" ]; then
echo "▶ Kotlin type check (compileStandardDebugKotlin)..."
(cd android && ./gradlew compileStandardDebugKotlin --quiet)

echo "▶ Android lint..."
(cd android && ./gradlew lintStandardDebug --quiet)

echo "▶ Android tests..."
npm run test:android
fi
26 changes: 26 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
included:
- ios

excluded:
- ios/Pods
- ios/build
- ios/OffgridMobile.xcodeproj
- ios/OffgridMobileTests

disabled_rules:
- trailing_whitespace # handled by editor
- line_length # RN bridge code has long lines

opt_in_rules:
- force_unwrapping

force_unwrapping:
severity: warning

function_body_length:
warning: 100
error: 200

type_body_length:
warning: 400
error: 1000
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sonarlint.connectedMode.project": {
"connectionId": "alichherawalla",
"projectKey": "alichherawalla_off-grid-mobile"
}
}
16 changes: 11 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

## Pre-Commit Quality Gates

Before EVERY commit, you MUST first check if you have added tests for whatever you are trying to commit. If not add tests. Then run all of the following checks and ensure they pass. Do NOT commit until all three are green:
All quality gates run automatically via Husky on every `git commit`, scoped to the file types you staged:

1. **Tests**: `npm test` — if you wrote or modified code, first ensure tests exist for the changes. Write missing tests before running.
2. **Linting**: `npm run lint`
3. **TypeScript**: `npx tsc --noEmit`
| Staged file type | Checks that run automatically |
|---|---|
| `.ts` / `.tsx` / `.js` / `.jsx` | eslint (staged only), `tsc --noEmit`, `npm test` |
| `.swift` | swiftlint (staged only), `npm run test:ios` |
| `.kt` / `.kts` | `compileStandardDebugKotlin` (type check), `lintStandardDebug`, `npm run test:android` |

Run all three in parallel. If any fail, fix the issues and re-run until they all pass. Never skip these checks.
**Requirements:**
- SwiftLint: `brew install swiftlint` (skipped with a warning if not installed)
- Android checks require the Gradle wrapper in `android/`

Before writing new code, ensure tests exist for your changes. If the hook fails, fix the issue and recommit — never skip with `--no-verify`.

## Push = Create PR + Address Review

Expand Down
29 changes: 13 additions & 16 deletions __tests__/integration/generation/generationFlow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe('Generation Flow Integration', () => {
let completeCallback: any = null;

mockLlmService.generateResponse.mockImplementation(
async (_messages, onStream, onComplete, _onError, _onThinking) => {
async (_messages, onStream, onComplete) => {
streamCallback = onStream!;
completeCallback = onComplete!;
return 'Hello world!';
Expand Down Expand Up @@ -118,7 +118,7 @@ describe('Generation Flow Integration', () => {
let completeCallback: any = null;

mockLlmService.generateResponse.mockImplementation(
async (_messages, onStream, onComplete, _onError, _onThinking) => {
async (_messages, onStream, onComplete) => {
streamCallback = onStream!;
completeCallback = onComplete!;
return 'Test';
Expand Down Expand Up @@ -153,7 +153,7 @@ describe('Generation Flow Integration', () => {
let completeCallback: any = null;

mockLlmService.generateResponse.mockImplementation(
async (_messages, onStream, onComplete, _onError, _onThinking) => {
async (_messages, onStream, onComplete) => {
streamCallback = onStream!;
completeCallback = onComplete!;
return 'Test';
Expand Down Expand Up @@ -186,7 +186,7 @@ describe('Generation Flow Integration', () => {
let completeCallback: any = null;

mockLlmService.generateResponse.mockImplementation(
async (_messages, _onStream, onComplete, _onError, _onThinking) => {
async (_messages, _onStream, onComplete) => {
completeCallback = onComplete!;
return 'Test';
}
Expand Down Expand Up @@ -214,7 +214,7 @@ describe('Generation Flow Integration', () => {
let completeCallback: any = null;

mockLlmService.generateResponse.mockImplementation(
async (_messages, onStream, onComplete, _onError, _onThinking) => {
async (_messages, onStream, onComplete) => {
streamCallback = onStream!;
completeCallback = onComplete!;
return 'Hello world';
Expand Down Expand Up @@ -254,7 +254,7 @@ describe('Generation Flow Integration', () => {
let completeCallback: any = null;

mockLlmService.generateResponse.mockImplementation(
async (_messages, onStream, onComplete, _onError, _onThinking) => {
async (_messages, onStream, onComplete) => {
streamCallback = onStream!;
completeCallback = onComplete!;
return 'Complete response';
Expand Down Expand Up @@ -316,7 +316,7 @@ describe('Generation Flow Integration', () => {
let completeCallback: any = null;

mockLlmService.generateResponse.mockImplementation(
async (_messages, onStream, onComplete, _onError, _onThinking) => {
async (_messages, onStream, onComplete) => {
streamCallback = onStream!;
completeCallback = onComplete!;
return 'Response';
Expand Down Expand Up @@ -347,11 +347,8 @@ describe('Generation Flow Integration', () => {
const modelId = setupWithActiveModel();
const conversationId = setupWithConversation({ modelId });

let _errorCallback: any = null;

mockLlmService.generateResponse.mockImplementation(
async (_messages, _onStream, _onComplete, onError, _onThinking) => {
_errorCallback = onError!;
async (_messages, _onStream, _onComplete) => {
throw new Error('Generation failed');
}
);
Expand All @@ -376,7 +373,7 @@ describe('Generation Flow Integration', () => {
const conversationId = setupWithConversation({ modelId });

mockLlmService.generateResponse.mockImplementation(
async (_messages, _onStream, _onComplete, _onError, _onThinking) => {
async (_messages) => {
// Never complete automatically - simulates ongoing generation
return new Promise(() => {});
}
Expand Down Expand Up @@ -432,7 +429,7 @@ describe('Generation Flow Integration', () => {
let streamCallback: any = null;

mockLlmService.generateResponse.mockImplementation(
async (_messages, onStream, _onComplete, _onError, _onThinking) => {
async (_messages, onStream) => {
streamCallback = onStream!;
// Simulate long running generation by returning a never-resolving promise
await new Promise(() => {});
Expand Down Expand Up @@ -481,7 +478,7 @@ describe('Generation Flow Integration', () => {
let streamCallback: any = null;

mockLlmService.generateResponse.mockImplementation(
async (_messages, onStream, _onComplete, _onError, _onThinking) => {
async (_messages, onStream) => {
streamCallback = onStream!;
return new Promise(() => {});
}
Expand Down Expand Up @@ -511,7 +508,7 @@ describe('Generation Flow Integration', () => {
const conversationId = setupWithConversation({ modelId });

mockLlmService.generateResponse.mockImplementation(
async (_messages, _onStream, _onComplete, _onError, _onThinking) => {
async (_messages) => {
return new Promise(() => {});
}
);
Expand Down Expand Up @@ -540,7 +537,7 @@ describe('Generation Flow Integration', () => {
let completeCallback: any = null;

mockLlmService.generateResponse.mockImplementation(
async (_messages, onStream, onComplete, _onError, _onThinking) => {
async (_messages, onStream, onComplete) => {
streamCallback = onStream!;
completeCallback = onComplete!;
return 'Test';
Expand Down
4 changes: 2 additions & 2 deletions __tests__/rntl/components/AnimatedPressable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ jest.mock('../../../src/utils/haptics', () => ({
triggerHaptic: jest.fn(),
}));

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { triggerHaptic: mockTriggerHaptic } = require('../../../src/utils/haptics');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Reanimated = require('react-native-reanimated');

describe('AnimatedPressable', () => {
Expand Down
Loading
Loading