Skip to content

feat: support multiple installed apps per repository (monorepo)#481

Merged
rainxchzed merged 2 commits into
mainfrom
feat/monorepo-support
May 2, 2026
Merged

feat: support multiple installed apps per repository (monorepo)#481
rainxchzed merged 2 commits into
mainfrom
feat/monorepo-support

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 2, 2026

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/ente ships 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):

  • Added getAppsByRepoId() and getAppsByRepoIdAsFlow() list-returning queries to InstalledAppDao alongside existing single-returning queries
  • Added matching methods to InstalledAppsRepository interface and implementation

Badge/status across all screens:

  • Home, Search, DevProfile: associateBy { it.repoId }groupBy { it.repoId } so installed/update badges reflect any tracked app per repo
  • Favourites & Starred: switched to list queries for install status

Details screen:

  • Added installedApps: List<InstalledApp> to DetailsState
  • loadInitial() fetches all installed apps via getAppsByRepoId()
  • observeInstalledApp() uses getAppsByRepoIdAsFlow(), picks primary by assetFilterRegex match

What Already Worked (no changes needed)

  • Background update checker (checkForUpdates per package name)
  • Export/import (keyed by packageName)
  • Advanced settings sheet (asset filter regex + fallback toggle)
  • DB schema (PK is packageName, not repoId)

Follow-up Work

  • Multi-app indicator UI: Show collapsible section in Details when installedApps.size > 1, listing each tracked app
  • SmartInstallButton enhancement: Show "N apps tracked" indicator for monorepo repos

Test 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

    • App installation tracking now accounts for multiple installed instances per repository; install/update badges and statuses reflect any tracked installation.
  • Documentation

    • Added new repository guidance documentation covering project setup, architecture conventions, and contribution guidance.

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>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a077bc34-2be9-46a3-87fc-56e2f3cdd7ca

📥 Commits

Reviewing files that changed from the base of the PR and between 8150451 and d54c1b1.

📒 Files selected for processing (1)
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt

Walkthrough

This 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.

Changes

Multi‑App Support Across Layers

Layer / File(s) Summary
Data Shape / DAO
core/data/.../local/db/dao/InstalledAppDao.kt
Replaced single-item repoId query with list-based APIs: getAppsByRepoIdAsFlow(repoId): Flow<List<InstalledAppEntity>> and added suspend getAppsByRepoId(repoId): List<InstalledAppEntity> (ordered by installedAt DESC).
Repository API
core/domain/.../repository/InstalledAppsRepository.kt
Added interface methods: suspend getAppsByRepoId(repoId): List<InstalledApp> and fun getAppsByRepoIdAsFlow(repoId): Flow<List<InstalledApp>>.
Repository Impl / Mapping
core/data/.../repository/InstalledAppsRepositoryImpl.kt
Implemented the new list-returning methods and mapped entity lists to domain models; added corresponding Flow mapping.
Dependent Repos / Business Logic
core/data/.../repository/FavouritesRepositoryImpl.kt, core/data/.../repository/StarredRepositoryImpl.kt
Switched to list-based DAO calls and derive a “primary” app as firstOrNull { !it.isPendingInstall } when populating stored entities (isInstalled/installedPackageName).
Presentation State
feature/details/presentation/.../DetailsState.kt
Added installedApps: List<InstalledApp> = emptyList() to hold multiple apps while installedApp remains the primary/selected entry.
Details ViewModel
feature/details/presentation/.../DetailsViewModel.kt
Observe list Flow (getAppsByRepoIdAsFlow) and compute primary by asset-regex match (fallback to first); loadInitial reconciles pending installs across apps using getAppsByRepoId, updates pending flags via PackageMonitor, and selects first reconciled app as installedApp.
Home / Search ViewModels
feature/home/presentation/.../HomeViewModel.kt, feature/search/presentation/.../SearchViewModel.kt
Change from associateBy { repoId } to groupBy { repoId }; per-repo flags (isInstalled, isUpdateAvailable) computed with apps.any { ... } across the grouped list.
Feature Repo Logic
feature/dev-profile/data/.../DeveloperProfileRepositoryImpl.kt
Switch install detection to installedApps.any { !it.isPendingInstall } so repo is installed if any tracked app finished installing.

Repository Guidance Doc

Layer / File(s) Summary
Documentation
AGENTS.md
New repository guidance covering project overview (Kotlin Multiplatform + Compose), setup (JDK 21+, GitHub OAuth client ID in local.properties), architecture conventions (Clean Architecture + MVVM, State/Action/Event), module/layout/navigation patterns, Koin DI conventions, cross-cutting rules (auth flow, X-GitHub-Token header, platform branching, Shizuku notes), coding conventions, feature onboarding steps, per-feature CLAUDE.md pointers, and version management location (gradle/libs.versions.toml).

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

"🐰 Hopped through code with tiny taps,
One repo now holds many app maps.
Pending flags cleared, primary picked with care,
Regex finds friends, and updates now share. 🥕"

🚥 Pre-merge checks | ✅ 1 | ❌ 4

❌ Failed checks (3 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR title 'feat: support multiple installed apps per repository (monorepo)' describes adding multi-app support, but the actual changes and PR objectives focus on resolving stuck 'preparing to install' status after self-update (#478). Align PR title with actual objectives: use 'fix: resolve stuck preparing to install after self-update' or similar to reflect the #478 issue being addressed.
Linked Issues check ⚠️ Warning PR objectives indicate fixing issue #478 (stuck isPendingInstall after self-update), but code changes only refactor app queries to support multiple apps per repo without addressing the self-update detection or PackageEventReceiver changes mentioned in objectives. Add missing implementation for GithubStoreApp.registerSelfAsInstalledApp(), PackageEventReceiver ACTION_MY_PACKAGE_REPLACED handling, and AndroidManifest intent-filter to resolve #478.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Changes refactor repository/DAO layers to support multiple installed apps per repo, which is in-scope for multi-app support but appears disconnected from PR objective to fix self-update stuck status in #478. Clarify whether monorepo multi-app support is foundational for #478 fix or if this is a separate feature; if separate, consider splitting into two PRs.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/monorepo-support

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.

❤️ Share
Review rate limit: 6/8 reviews remaining, refill in 14 minutes and 36 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@rainxchzed rainxchzed changed the title fix: resolve stuck 'preparing to install' after self-update (#478) feat: support multiple installed apps per repository (monorepo) May 2, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 win

Uncaught PatternSyntaxException can crash the flow collection.

If assetFilterRegex contains an invalid regex pattern, Regex(filter) will throw PatternSyntaxException, terminating the collect loop 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 value

Simplify the global update badge — installedApps is already in scope.

installedMap.values.flatten() reconstructs the full list from the already-grouped map when installedApps (the raw pre-grouped list from the collect lambda) 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

📥 Commits

Reviewing files that changed from the base of the PR and between d12a5a6 and 8150451.

📒 Files selected for processing (11)
  • AGENTS.md
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/FavouritesRepositoryImpl.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/StarredRepositoryImpl.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt
  • feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.kt
  • feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt
  • feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt

Comment thread AGENTS.md

### Module Layout

```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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>
@rainxchzed rainxchzed merged commit a9882c0 into main May 2, 2026
1 check was pending
@rainxchzed rainxchzed deleted the feat/monorepo-support branch May 2, 2026 04:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow installing and updating multiple assets from a single repository error in downloading other app from the same repository

1 participant