Model Availability UX Overhaul — StateBadge, ModelCard, unified download UI#1067
Merged
Conversation
…dModelStatus, truth-table tests
…oadSource, deprecation filter
…ModelCardCompact + Manage link
Debug APK readyCommit: Updated on each push. Removed when PR is merged or closed. |
- Replace 3-frame debounce open with fast-open/slow-close semantics: a single above-threshold frame immediately resets silence counter (opens gate in 80ms instead of 240ms) - Keep 3-frame debounce for closing: only after 3 consecutive silent frames does the silence timer begin accumulating - Lower silenceRmsThreshold 600→300 to catch quieter speech - Reduce maxSilenceSkipSeconds 3.0→1.0 for more frequent periodic checks during gating - Update alive logging, WakeWordSilenceGateTest Oracle review confirmed: VAD gate was the root cause of inconsistent wake word detection — the model confidence is binary (~0.0009 or ~0.9) and failures occur when the gate starves the classifier of onset context.
…ility-ux # Conflicts: # core/voice/src/main/java/com/kernel/ai/core/voice/OnnxWakeWordDetector.kt
- Fix downloadSources race (atomic update) + cancelDownload source guard - Replace StateBadge AssistChip with non-clickable Surface (accessibility) - Use displaySummary on Settings screen (instead of hardcoded string) - Move storage calcs off main thread (IO dispatcher + separate flow) - Restore Update action for Ready models; fix LicenseRequired URL handling - Wire hfAuth + downloadSources through Chat onboarding - Restore download/cancel/delete action buttons in VoiceScreen - Wire GatedModelStatusRepository into ModelManagement/Settings VMs
…pact for download progress
There was a problem hiding this comment.
Pull request overview
Introduces a new canonical “model availability” UX system (:core:model-availability) and integrates it across Settings, Chat onboarding, Model Management, and Voice screens to unify availability state badges/cards and download actions.
Changes:
- Added
:core:model-availabilitywithModelAvailabilityState, mapping logic + tests,StateBadge,ModelCard/ModelCardCompact, andAvailabilitySummary. - Integrated the new availability UI across Settings/Chat/Voice/Model Management, including navigation entry points to Model Management.
- Extended download plumbing with
DownloadSourcetracking and auto-queue/co-queue behavior (e.g., SentencePiece), plus gated-model status persistence via DataStore.
Reviewed changes
Copilot reviewed 36 out of 37 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| settings.gradle.kts | Adds the new :core:model-availability module to the build. |
| feature/settings/build.gradle.kts | Depends on :core:model-availability for shared UI/state mapping. |
| feature/settings/src/main/java/com/kernel/ai/feature/settings/SettingsScreen.kt | Replaces HF row with tappable model availability summary row. |
| feature/settings/src/main/java/com/kernel/ai/feature/settings/SettingsViewModel.kt | Computes AvailabilitySummary and wires gated model status + download sources. |
| feature/settings/src/main/java/com/kernel/ai/feature/settings/ModelManagementScreen.kt | Replaces per-row UI with ModelCard driven by availability state. |
| feature/settings/src/main/java/com/kernel/ai/feature/settings/ModelManagementViewModel.kt | Adds download source + gated status + storage metrics + availability summary. |
| feature/settings/src/main/java/com/kernel/ai/feature/settings/ModelSettingsScreen.kt | Shows StateBadge in model settings headers and renames internal card composable. |
| feature/settings/src/main/java/com/kernel/ai/feature/settings/ModelSettingsViewModel.kt | Adds model availability state to model-settings UI state. |
| feature/settings/src/main/java/com/kernel/ai/feature/settings/VoiceScreen.kt | Replaces bespoke voice/STT download UI with ModelCardCompact + external actions + navigation link. |
| feature/settings/src/main/java/com/kernel/ai/feature/settings/VoiceViewModel.kt | Maps voice/STT download states into ModelAvailabilityState for compact cards. |
| feature/settings/src/main/java/com/kernel/ai/feature/settings/MemoryScreen.kt | Uses extracted CollapsibleSectionHeader from :core:ui. |
| feature/chat/build.gradle.kts | Depends on :core:model-availability. |
| feature/chat/src/main/java/com/kernel/ai/feature/chat/model/ChatUiState.kt | Adds HF auth + download source data to onboarding UI state. |
| feature/chat/src/main/java/com/kernel/ai/feature/chat/ChatViewModel.kt | Feeds onboarding with downloadSources and HF auth state. |
| feature/chat/src/main/java/com/kernel/ai/feature/chat/ChatScreen.kt | Uses ModelCardCompact in onboarding + “Manage models” navigation. |
| feature/chat/src/test/java/com/kernel/ai/feature/chat/ChatViewModelVoiceTest.kt | Updates tests for new ChatViewModel constructor deps. |
| feature/chat/src/test/java/com/kernel/ai/feature/chat/ChatViewModelInitTest.kt | Updates tests for new ChatViewModel constructor deps. |
| app/src/main/java/com/kernel/ai/navigation/KernelNavHost.kt | Adds navigation wiring to Model Management from Chat/Voice. |
| core/ui/src/main/java/com/kernel/ai/core/ui/CollapsibleSectionHeader.kt | New shared UI component extracted from Memory screen. |
| core/voice/src/main/java/com/kernel/ai/core/voice/OnnxWakeWordDetector.kt | Adjusts gating/check placement in wake word pipeline. |
| core/inference/src/main/java/com/kernel/ai/core/inference/download/DownloadSource.kt | New enum to distinguish auto-queued vs user-initiated downloads. |
| core/inference/src/main/java/com/kernel/ai/core/inference/download/KernelModel.kt | Adds isDeprecated flag and deprecates SM8550 model entry. |
| core/inference/src/main/java/com/kernel/ai/core/inference/download/ModelDownloadManager.kt | Tracks per-model DownloadSource, changes auto-queue behavior, adds cancel guard, co-queues SentencePiece. |
| core/inference/src/test/java/com/kernel/ai/core/inference/download/KernelModelTest.kt | Adds regression tests for deprecated model behavior. |
| core/model-availability/build.gradle.kts | New module build config and dependencies (Compose + DataStore). |
| core/model-availability/src/main/java/com/kernel/ai/core/model/availability/ModelAvailabilityState.kt | Defines canonical 4-state model availability sealed class + reasons. |
| core/model-availability/src/main/java/com/kernel/ai/core/model/availability/DownloadStateMapper.kt | Maps DownloadState + context (auth/source/gating) → availability state. |
| core/model-availability/src/main/java/com/kernel/ai/core/model/availability/StateBadge.kt | UI badge for availability state. |
| core/model-availability/src/main/java/com/kernel/ai/core/model/availability/ModelCard.kt | Shared ModelCard/ModelCardCompact composables. |
| core/model-availability/src/main/java/com/kernel/ai/core/model/availability/ModelAvailabilityStrings.kt | Centralizes user-facing labels/supporting text for states. |
| core/model-availability/src/main/java/com/kernel/ai/core/model/availability/AvailabilitySummary.kt | Computes and formats overall model readiness summary. |
| core/model-availability/src/main/java/com/kernel/ai/core/model/availability/GatedModelStatus.kt | Defines persisted gated-model status enum. |
| core/model-availability/src/main/java/com/kernel/ai/core/model/availability/GatedModelStatusRepository.kt | DataStore repository for gated model statuses. |
| core/model-availability/src/test/java/com/kernel/ai/core/model/availability/DownloadStateMapperTest.kt | Truth-table style tests for the mapper. |
| docs/manual-test-scenarios-1067.md | Adds detailed device test plan for this PR. |
| docs/agents/model-availability-ux-patterns.md | Documents the canonical state machine and file locations. |
| .omp/AGENTS.md | Updates architecture docs and codifies subagent recovery pattern. |
Comment on lines
+56
to
+64
| viewModelScope.launch { | ||
| val gatedModels = KernelModel.entries.filter { | ||
| it.showInModelManagement && !it.isDeprecated && it.isGated | ||
| } | ||
| gatedModels.forEach { model -> | ||
| gatedModelStatusRepository.get(model).collect { status -> | ||
| _gatedStatuses.update { it.toMutableMap().apply { put(model, status) } } | ||
| } | ||
| } |
Comment on lines
+72
to
+80
| viewModelScope.launch { | ||
| val gatedModels = KernelModel.entries.filter { | ||
| it.showInModelManagement && !it.isDeprecated && it.isGated | ||
| } | ||
| gatedModels.forEach { model -> | ||
| gatedModelStatusRepository.get(model).collect { status -> | ||
| _gatedStatuses.update { it.toMutableMap().apply { put(model, status) } } | ||
| } | ||
| } |
Comment on lines
+47
to
+58
| viewModelScope.launch { | ||
| modelDownloadManager.downloadStates.collect { states -> | ||
| _uiState.update { current -> | ||
| current.copy( | ||
| e2bAvailability = states[KernelModel.GEMMA_4_E2B] | ||
| ?.toAvailability(KernelModel.GEMMA_4_E2B, hfAuth = false), | ||
| e4bAvailability = states[KernelModel.GEMMA_4_E4B] | ||
| ?.toAvailability(KernelModel.GEMMA_4_E4B, hfAuth = false), | ||
| ) | ||
| } | ||
| } | ||
| } |
Comment on lines
+120
to
+141
| when (state) { | ||
| is ModelAvailabilityState.Preparing -> { | ||
| // Auto-queued: no action button; User-initiated: show cancel | ||
| if (!state.isAutoQueued) { | ||
| Button( | ||
| onClick = { onPrimaryAction?.invoke() }, | ||
| modifier = Modifier.fillMaxWidth(), | ||
| ) { | ||
| Text(actionLabel ?: "Cancel") | ||
| } | ||
| } | ||
| } | ||
| is ModelAvailabilityState.Unavailable, | ||
| ModelAvailabilityState.NotDisplayed -> { | ||
| // Full-width outlined button for unavailable/not-displayed | ||
| OutlinedButton( | ||
| onClick = onPrimaryAction, | ||
| modifier = Modifier.fillMaxWidth(), | ||
| ) { | ||
| Text(actionLabel) | ||
| } | ||
| } |
Comment on lines
+25
to
+33
| /** | ||
| * Compact state badge chip for model availability. | ||
| * | ||
| * Uses M3 `AssistChip` shape at 28dp height with an icon + label. | ||
| * Maps [ModelAvailabilityState] to the appropriate icon and color scheme. | ||
| * | ||
| * @param state The availability state to render. | ||
| * @param modifier Modifier for the chip. | ||
| */ |
… unused imports, trailing newline, redundant co-queue
- ModelCard: Add onSecondaryAction + secondaryActionLabel params, renders secondary (Delete) button beside primary for Ready state - ModelManagementViewModel.deleteModel: Block deletion only if model is the currently selected preferred model, not isRequired - ModelManagementScreen: Wire delete action for downloaded models that aren't bundled or the current preferred model
… code - deleteModel: Keep isRequired guard for infrastructure models (EmbeddingGemma, SP tokenizer); only allow E2B↔E4B swap when the other is explicitly selected - Remove dead ModelRow and HuggingFaceRow composables (replaced by ModelCard) - Remove unused imports left behind by dead code removal
Comment on lines
+56
to
+64
| viewModelScope.launch { | ||
| val gatedModels = KernelModel.entries.filter { | ||
| it.showInModelManagement && !it.isDeprecated && it.isGated | ||
| } | ||
| gatedModels.forEach { model -> | ||
| gatedModelStatusRepository.get(model).collect { status -> | ||
| _gatedStatuses.update { it.toMutableMap().apply { put(model, status) } } | ||
| } | ||
| } |
Comment on lines
+72
to
+80
| viewModelScope.launch { | ||
| val gatedModels = KernelModel.entries.filter { | ||
| it.showInModelManagement && !it.isDeprecated && it.isGated | ||
| } | ||
| gatedModels.forEach { model -> | ||
| gatedModelStatusRepository.get(model).collect { status -> | ||
| _gatedStatuses.update { it.toMutableMap().apply { put(model, status) } } | ||
| } | ||
| } |
Comment on lines
+454
to
+461
| sttState.issue != null -> { | ||
| OutlinedButton( | ||
| modifier = Modifier.padding(horizontal = 16.dp, vertical = 2.dp), | ||
| onClick = { onDownloadSherpaStt(engine) }, | ||
| ) { | ||
| Text("Retry") | ||
| } | ||
| } |
Comment on lines
+8
to
+12
| * Maps the core [DownloadState] to the UI-layer [ModelAvailabilityState]. | ||
| * | ||
| * Truth table (see docs/model-availability-ux-patterns.md): | ||
| * | ||
| * | DownloadState | isBundled | isGated | hfAuth | source | gated | → Result | |
Comment on lines
+21
to
+25
| @Test | ||
| fun `deprecated model is excluded from preferredForTier matches`() { | ||
| // SM8550 is deprecated — it should not match any tier preference logic | ||
| assertTrue(KernelModel.EMBEDDING_GEMMA_300M_SM8550.isDeprecated) | ||
| } |
Comment on lines
+32
to
+53
| - Downloaded models show `Ready` badge (green `CheckCircle`) | ||
| - Downloading models show `Preparing` (amber `HourglassEmpty`) | ||
| - `Preparing` shows `Downloading` label for user-initiated, `Waiting` for auto-queued | ||
| - Error state shows `Failed` badge (red `WarningAmber`) | ||
| - Gated models not yet authenticated show `Sign in` badge | ||
| - HuggingFace row is **removed** from Model Management (moved to account section) | ||
| - Deprecated model (SM8550) is not visible in the list | ||
|
|
||
| --- | ||
|
|
||
| ### 3. Action buttons on ModelCard | ||
| **Steps:** | ||
| 1. For a `NotDownloaded` model — tap the model card's button | ||
| 2. For a `Downloading` model — tap Cancel | ||
| 3. For a `DownloadFailed` model — tap Retry | ||
|
|
||
| **Expected:** | ||
| - `NotDownloaded` → tap starts download, badge transitions to `Preparing` | ||
| - `Downloading` → Cancel stops the download (unless `isRequired`) | ||
| - `DownloadFailed` → Retry restarts download | ||
| - Required models (`isRequired = true`) — Cancel button is **not shown** | ||
| - Action button is full-width `Button` (filled) for actionable states, `OutlinedButton` for Unavailable |
Comment on lines
+117
to
+126
| ### 8. Cancel download guard — required models | ||
| **Steps:** | ||
| 1. While a required model (e.g. E2B or E4B) is downloading | ||
| 2. Try to cancel it from the UI | ||
|
|
||
| **Expected:** | ||
| - Cancel button is **not shown** for required models | ||
| - If cancellation is attempted programmatically, `cancelDownload()` logs a warning and returns without cancelling | ||
| - `isRequired` guard covers both UI and programmatic paths | ||
|
|
Comment on lines
+132
to
+141
| is ModelAvailabilityState.Unavailable, | ||
| ModelAvailabilityState.NotDisplayed -> { | ||
| // Full-width outlined button for unavailable/not-displayed | ||
| OutlinedButton( | ||
| onClick = onPrimaryAction, | ||
| modifier = Modifier.fillMaxWidth(), | ||
| ) { | ||
| Text(actionLabel) | ||
| } | ||
| } |
… dead code - P1: Fix GatedModelStatus collection blocking on first model in both ModelManagementViewModel and SettingsViewModel — use launch per model - P2: Add download-in-progress guard to deleteModel (cancel before delete) - P3: Remove dead HfOrange/EMBEDDING_GEMMA_LICENCE_URL constants - P3: Simplify stale SM8550 filter in scroll index calculation
Owner
Owner
|
Deleting e4b worked even though it was selected. No other models can be deleted - the button presses but nothing happens. |
Root cause: deleteModel had a stricter guard (isConversationSwap + isRequired) than the screen's canDelete (isBundled + preferredModel), causing delete buttons to appear for models that deleteModel silently refused to delete. - Remove isConversationSwap/isRequired guard from deleteModel - Keep only: isBundled return, preferredModel return, download-in-progress cancel - Screen canDelete already matches: Ready && !isBundled && != preferredModel - User can now delete any non-bundled non-selected downloaded model
- P2: Fix auth-guard ordering in onPrimaryAction — auth only checked for NotDownloaded state, so Cancel/Update always work regardless of auth - P3: Debounce storage metrics recalculation (500ms) to reduce I/O - P3: Fix AvailabilitySummary label — exclude Unavailable from ready count - P3: Fix indentation drift in OnnxWakeWordDetector gating block
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1043, #1029, #1030, #1035, #1038, #1041
What
New
:core:model-availabilitymodule providing a canonical 4-state model availability system (Ready / Preparing / ActionRequired / Unavailable) withStateBadge+ModelCard/ModelCardCompactcomposables, integrated across Settings, Chat, and Voice screens.Changes (20 commits)
Core module
:core:model-availability—ModelAvailabilityStatesealed class,DownloadStateMapperwith truth-table tests,GatedModelStatusDataStore repository (scaffold),AvailabilitySummary,ModelAvailabilityStrings:core:inference—KernelModel.isDeprecatedfield,DownloadSourceenum,cancelDownloadisRequiredguard, SentencePiece co-queue in init:core:ui— extractedCollapsibleSectionHeaderfrom MemoryScreenComposable library
StateBadge— small pill showing availability state with colorModelCard— full card with action button, lock icon, progress barModelCardCompact— slim inline variant for onboarding/voiceScreen integration
ModelCardwith state badgesModelCard→ModelSettingsCard; addedStateBadgein card headersModelProgressRow→ModelCardCompactModelCardCompact+ Manage linksDocs
docs/agents/model-availability-ux-patterns.md.omp/AGENTS.mdCode review fixes (b6e1abe)
Applied all findings from oracle code review:
ModelDownloadManager.updateState()DownloadSource(auto-queued) instead ofmodel.isRequiredAssistChip(onClick={})with non-clickableSurfacedisplaySummaryinstead of hardcoded${ready} of ${total}hfAuthanddownloadSourcesthrough totoAvailability(); added "Manage models" buttonModelCardCompactModelManagementViewModelandSettingsViewModelauthRepositorymock to ChatViewModel testsPre-merge gates
Manual device test scenarios
1. Fresh install — auto-queue flow
Steps:
Expected:
ModelCardCompactfor each auto-queued modelPreparing (Waiting)for auto-queued models not yet startedPreparing (Downloading)once download begins2. Model Management — state badges + actions
Steps:
Expected:
Readybadge (greenCheckCircle) with "Update" buttonPreparing(amberHourglassEmpty)PreparingshowsDownloadinglabel for user-initiated,Waitingfor auto-queuedFailedbadge (redWarningAmber)Sign inbadgeNotDownloaded→ tap starts download, badge transitions toPreparingDownloading→ Cancel stops the download (unless auto-queued)DownloadFailed→ Retry restarts download3. Chat onboarding — ModelCardCompact
Steps:
Expected:
ModelCardCompactwith: model name, size label, state badge4. Voice screen — ModelCardCompact for voices/STT
Steps:
Expected:
ModelCardCompactwith state badge + download/cancel/delete buttonsModelCardCompact+ radio button + download/cancel/delete buttonsModelCardCompact+ radio button + download/cancel/delete buttonsReadybadge, radio button enabledNot availablebadge, "Download" button shownPreparingbadge with progress + "Cancel" buttonTextButtonnavigates to Model Management5. Settings screen — model availability summary
Steps:
Expected:
displaySummary: "X of Y models ready" (includes unavailable models in count)6. Model Settings screen — StateBadge
Steps:
Expected:
StateBadgenext to model name7. Cancel download guard
Steps:
Expected:
cancelDownload()on auto-queued model logs a warning and returns without cancelling8. HuggingFace auth — gated model states
Steps:
Expected:
ActionRequired (SignInRequired)badgeGatedModelStatusRepositorypersists status across app restarts9. State survival across config changes
Steps:
Expected:
10. Navigation — Model Management route
Steps:
Expected:
11. Regression — existing download states unchanged
Steps:
Expected:
Readybadge immediatelyisDownloaded()check prevents re-queuingMINI_LM) showReadybadge even without downloadTest results
:core:model-availability:test— pass:core:inference:test— pass:feature:settings:compileDebugKotlin— pass:feature:chat:compileDebugKotlin— passChatViewModelInitTestcoroutine issues unrelated)