feat: support multiple installed apps per repository (monorepo)#481
Conversation
Remove the one-repo-to-one-app assumption that prevented installing and tracking multiple apps from the same GitHub repository (e.g. ente-io/ente ships Auth, Photos, Locker in separate releases). Changes: - Add getAppsByRepoId / getAppsByRepoIdAsFlow list-returning queries to InstalledAppDao, InstalledAppsRepository, and its implementation - Switch Home, Search, DevProfile from associateBy to groupBy so installed/update badges reflect *any* tracked app per repo - Update Favourites and Starred to use list queries for install status - Add installedApps: List<InstalledApp> to DetailsState alongside the existing installedApp (primary) for multi-app awareness - DetailsViewModel.loadInitial and observeInstalledApp now fetch and observe all tracked apps per repo, picking the primary by asset filter regex match No Room DB migration needed — PK is packageName, so multiple rows with different package names but same repoId already coexist. Background update checker, export/import, and advanced settings sheet all work without changes. Closes #471, Closes #420 Co-Authored-By: Oz <oz-agent@warp.dev>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR adds first-class support for multiple installed apps per repository: DAO and repository APIs now return lists, domain/repo implementations map lists, state and ViewModels observe/group apps by repo and compute install/update status from any non-pending app, and a new AGENTS.md doc is added. ChangesMulti‑App Support Across Layers
Repository Guidance Doc
Sequence Diagram(s)sequenceDiagram
participant UI as ViewModel/UI
participant Repo as InstalledAppsRepository
participant DAO as InstalledAppDao
participant DB as Local DB
UI->>Repo: observe apps for repoId (getAppsByRepoIdAsFlow)
Repo->>DAO: query Flow<List<InstalledAppEntity>>
DAO->>DB: SELECT * FROM installed_apps WHERE repoId ORDER BY installedAt DESC
DB-->>DAO: emit rows[]
DAO-->>Repo: emit entities[]
Repo-->>UI: emit domain InstalledApp list
UI->>UI: select primary (regex match or first) and recompute status/insights
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 1 | ❌ 4❌ Failed checks (3 warnings, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Review rate limit: 6/8 reviews remaining, refill in 14 minutes and 36 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt (1)
821-855:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUncaught
PatternSyntaxExceptioncan crash the flow collection.If
assetFilterRegexcontains an invalid regex pattern,Regex(filter)will throwPatternSyntaxException, terminating thecollectloop and leaving the UI in a broken state. Wrap the regex construction in a try-catch.🛡️ Proposed fix to handle invalid regex gracefully
val primary = apps.firstOrNull { existing -> _state.value.primaryAsset?.name?.let { assetName -> val filter = existing.assetFilterRegex - filter != null && Regex(filter).containsMatchIn(assetName) + if (filter == null) return@let false + try { + Regex(filter).containsMatchIn(assetName) + } catch (_: Exception) { + false + } } == true } ?: apps.firstOrNull()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt` around lines 821 - 855, The collect block in observeInstalledApp can crash if existing.assetFilterRegex is invalid because Regex(filter) will throw PatternSyntaxException; wrap the regex construction/match (the lambda that computes primary using Regex(filter).containsMatchIn(assetName)) in a try-catch (or runCatching) and treat failures as non-matching so the flow continues, optionally logging the error; update the primary selection logic in observeInstalledApp (the firstOrNull { existing -> ... } block) to handle invalid patterns safely and avoid abandoning the collection.
🧹 Nitpick comments (1)
feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt (1)
120-120: 💤 Low valueSimplify the global update badge —
installedAppsis already in scope.
installedMap.values.flatten()reconstructs the full list from the already-grouped map wheninstalledApps(the raw pre-grouped list from thecollectlambda) is still available.♻️ Proposed simplification
- isUpdateAvailable = installedMap.values.flatten().any { it.hasActualUpdate() }, + isUpdateAvailable = installedApps.any { it.hasActualUpdate() },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt` at line 120, Replace the costly reconstruction installedMap.values.flatten().any { it.hasActualUpdate() } with the already-in-scope list installedApps.any { it.hasActualUpdate() } inside HomeViewModel (use the installedApps variable from the collect lambda) so the badge check uses the original pre-grouped list and keeps the hasActualUpdate() call unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@AGENTS.md`:
- Line 54: The markdown fenced code block that shows the module/tree block is
missing a language identifier (triggering MD040); update the opening fence from
``` to include a language tag such as ```text (or ```md/```bash as appropriate)
so the block is explicitly tagged and the linter warning is resolved.
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt`:
- Around line 29-33: The queries in InstalledAppDao (getAppsByRepoId and
getAppsByRepoIdAsFlow) lack an ORDER BY, so calls like
FavouritesRepositoryImpl.addFavorite and StarredRepositoryImpl.syncStarredRepos
that use firstOrNull can return non-deterministic rows for monorepos; update
both `@Query` annotations to add a deterministic ORDER BY (e.g., ORDER BY
installed_package_name ASC, or another stable column such as id or created_at)
so the result ordering is stable across calls and firstOrNull always picks the
same row.
---
Outside diff comments:
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt`:
- Around line 821-855: The collect block in observeInstalledApp can crash if
existing.assetFilterRegex is invalid because Regex(filter) will throw
PatternSyntaxException; wrap the regex construction/match (the lambda that
computes primary using Regex(filter).containsMatchIn(assetName)) in a try-catch
(or runCatching) and treat failures as non-matching so the flow continues,
optionally logging the error; update the primary selection logic in
observeInstalledApp (the firstOrNull { existing -> ... } block) to handle
invalid patterns safely and avoid abandoning the collection.
---
Nitpick comments:
In
`@feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt`:
- Line 120: Replace the costly reconstruction installedMap.values.flatten().any
{ it.hasActualUpdate() } with the already-in-scope list installedApps.any {
it.hasActualUpdate() } inside HomeViewModel (use the installedApps variable from
the collect lambda) so the badge check uses the original pre-grouped list and
keeps the hasActualUpdate() call unchanged.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4eed94bd-af18-41a0-b6ef-a9518c9dfcfe
📒 Files selected for processing (11)
AGENTS.mdcore/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/FavouritesRepositoryImpl.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/StarredRepositoryImpl.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.ktfeature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.ktfeature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.ktfeature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt
|
|
||
| ### Module Layout | ||
|
|
||
| ``` |
There was a problem hiding this comment.
Add a language identifier to the fenced block
The code fence at Line 54 is missing a language tag (MD040). Use something like ```text for the module tree block to clear lint warnings.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 54-54: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@AGENTS.md` at line 54, The markdown fenced code block that shows the
module/tree block is missing a language identifier (triggering MD040); update
the opening fence from ``` to include a language tag such as ```text (or
```md/```bash as appropriate) so the block is explicitly tagged and the linter
warning is resolved.
…/dao/InstalledAppDao.kt Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Summary
Remove the one-repo-to-one-app assumption that prevented installing and tracking multiple apps from the same GitHub repository.
Closes #471, Closes #420
Problem
Repos that ship multiple distinct apps in their releases (e.g.
ente-io/enteships Auth + Photos + Locker, Proton ships Mail + VPN + Drive) could only have one app tracked. Installing a second app from the same repo was blocked, and update checking only tracked the first installed asset.What Changed
Core layer (DAO + Repository):
getAppsByRepoId()andgetAppsByRepoIdAsFlow()list-returning queries toInstalledAppDaoalongside existing single-returning queriesInstalledAppsRepositoryinterface and implementationBadge/status across all screens:
associateBy { it.repoId }→groupBy { it.repoId }so installed/update badges reflect any tracked app per repoDetails screen:
installedApps: List<InstalledApp>toDetailsStateloadInitial()fetches all installed apps viagetAppsByRepoId()observeInstalledApp()usesgetAppsByRepoIdAsFlow(), picks primary byassetFilterRegexmatchWhat Already Worked (no changes needed)
checkForUpdatesper package name)packageName)packageName, notrepoId)Follow-up Work
installedApps.size > 1, listing each tracked appTest with
ente-io/ente(ships auth, photos, locker)AChep/keyguard-app(multiple APK variants)nicehash/NiceHashQuickMiner(multiple tools)Warp conversation
Implementation plan
Co-Authored-By: Oz oz-agent@warp.dev
Summary by CodeRabbit
New Features
Documentation