Skip to content

Model Availability UX Overhaul — StateBadge, ModelCard, unified download UI#1067

Merged
NickMonrad merged 29 commits into
mainfrom
feature/model-availability-ux
Jun 4, 2026
Merged

Model Availability UX Overhaul — StateBadge, ModelCard, unified download UI#1067
NickMonrad merged 29 commits into
mainfrom
feature/model-availability-ux

Conversation

@lokhor
Copy link
Copy Markdown
Collaborator

@lokhor lokhor commented Jun 2, 2026

Closes #1043, #1029, #1030, #1035, #1038, #1041

What

New :core:model-availability module providing a canonical 4-state model availability system (Ready / Preparing / ActionRequired / Unavailable) with StateBadge + ModelCard/ModelCardCompact composables, integrated across Settings, Chat, and Voice screens.

Changes (20 commits)

Core module

  • :core:model-availabilityModelAvailabilityState sealed class, DownloadStateMapper with truth-table tests, GatedModelStatus DataStore repository (scaffold), AvailabilitySummary, ModelAvailabilityStrings
  • :core:inferenceKernelModel.isDeprecated field, DownloadSource enum, cancelDownload isRequired guard, SentencePiece co-queue in init
  • :core:ui — extracted CollapsibleSectionHeader from MemoryScreen

Composable library

  • StateBadge — small pill showing availability state with color
  • ModelCard — full card with action button, lock icon, progress bar
  • ModelCardCompact — slim inline variant for onboarding/voice

Screen integration

  • ModelManagementScreen — HuggingFace row removed; uses ModelCard with state badges
  • ModelSettingsScreen — renamed private ModelCardModelSettingsCard; added StateBadge in card headers
  • SettingsScreen — HuggingFace row removed; model availability summary row added
  • ChatScreen — onboarding ModelProgressRowModelCardCompact
  • VoiceScreen — inline download composables (~400 lines) replaced with ModelCardCompact + Manage links

Docs

  • docs/agents/model-availability-ux-patterns.md
  • Subagent recovery pattern codified in .omp/AGENTS.md

Code review fixes (b6e1abe)

Applied all findings from oracle code review:

  • Atomic downloads map updates — fixed read-copy-write race in ModelDownloadManager.updateState()
  • cancelDownload guard — now checks DownloadSource (auto-queued) instead of model.isRequired
  • StateBadge accessibility — replaced AssistChip(onClick={}) with non-clickable Surface
  • Settings summary — uses displaySummary instead of hardcoded ${ready} of ${total}
  • Main-thread I/O — storage calcs moved to IO dispatcher via separate flow
  • Model Management actions — "Update" button for Ready models; LicenseRequired opens licence URL
  • Chat onboarding — passes hfAuth and downloadSources through to toAvailability(); added "Manage models" button
  • VoiceScreen actions — restored download/cancel/delete buttons alongside ModelCardCompact
  • Gated model status repository — wired into ModelManagementViewModel and SettingsViewModel
  • Test fix — added authRepository mock to ChatViewModel tests

Pre-merge gates

  • Device smoke test via ADB (no merge until verified)
  • Owner approval

Manual device test scenarios

1. Fresh install — auto-queue flow

Steps:

  1. Clear app data or fresh install
  2. Launch app → observe onboarding screen
  3. Wait for download manager init (~2s)

Expected:

  • Chat onboarding shows ModelCardCompact for each auto-queued model
  • Each card shows the correct state badge:
    • Preparing (Waiting) for auto-queued models not yet started
    • Preparing (Downloading) once download begins
  • Progress updates in real time (no stale 0% stuck)
  • Model count summary "X of Y models ready" on Settings screen starts at "0 of Y"

2. Model Management — state badges + actions

Steps:

  1. Navigate to Settings → Model availability
  2. Observe each model card
  3. Interact with action buttons

Expected:

  • Downloaded models show Ready badge (green CheckCircle) with "Update" button
  • 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
  • NotDownloaded → tap starts download, badge transitions to Preparing
  • Downloading → Cancel stops the download (unless auto-queued)
  • DownloadFailed → Retry restarts download
  • LicenseRequired → opens licence URL in Custom Tab
  • HuggingFace row is removed from Model Management
  • Deprecated model (SM8550) is not visible in the list

3. Chat onboarding — ModelCardCompact

Steps:

  1. Fresh install (or delete models and restart)
  2. Observe the onboarding progress section

Expected:

  • Each model shows ModelCardCompact with: model name, size label, state badge
  • Lock icon shown for gated models; sign-in badge if not authenticated
  • Tapping "Manage models" navigates to Model Management screen

4. Voice screen — ModelCardCompact for voices/STT

Steps:

  1. Navigate to Settings → Voice
  2. Expand Sherpa-ONNX section
  3. Observe STT and voice model cards (Sherpa Piper, Kokoro)

Expected:

  • Each STT engine shows ModelCardCompact with state badge + download/cancel/delete buttons
  • Sherpa Piper voices show ModelCardCompact + radio button + download/cancel/delete buttons
  • Kokoro voices show ModelCardCompact + radio button + download/cancel/delete buttons
  • Downloaded voices show Ready badge, radio button enabled
  • Not-downloaded voices show Not available badge, "Download" button shown
  • Downloading voices show Preparing badge with progress + "Cancel" button
  • "Manage voice models" TextButton navigates to Model Management

5. Settings screen — model availability summary

Steps:

  1. Navigate to Settings
  2. Observe the new "Model availability" row

Expected:

  • Row shows displaySummary: "X of Y models ready" (includes unavailable models in count)
  • Count matches observed states
  • Tapping row navigates to Model Management screen
  • HuggingFace account row is removed from Settings

6. Model Settings screen — StateBadge

Steps:

  1. Navigate to Settings → Model settings
  2. Observe E2B and E4B card headers

Expected:

  • Each card header shows StateBadge next to model name
  • Badge reflects current download/availability state
  • Badge updates live as download state changes

7. Cancel download guard

Steps:

  1. While an auto-queued required model is downloading, try to cancel

Expected:

  • Cancel button is not shown for auto-queued models
  • User-initiated downloads can always be cancelled
  • Programmatic cancelDownload() on auto-queued model logs a warning and returns without cancelling

8. HuggingFace auth — gated model states

Steps:

  1. Without HF auth, observe a gated model (e.g. EmbeddingGemma-300M)
  2. Sign in to HuggingFace
  3. Check gated model status after sign-in

Expected:

  • Without auth: gated model shows ActionRequired (SignInRequired) badge
  • "Sign in to HuggingFace" button appears on ModelCard
  • GatedModelStatusRepository persists status across app restarts

9. State survival across config changes

Steps:

  1. Start a download
  2. Rotate the device (or trigger config change)
  3. Observe all screens

Expected:

  • Download progress survives rotation (ViewModel + WorkManager)
  • State badges remain correct after rotation
  • No Compose recomposition crashes or NPEs

10. Navigation — Model Management route

Steps:

  1. From Chat onboarding → tap "Manage models" → verify navigation
  2. From Settings → tap "Model availability" row → verify navigation
  3. From Voice screen → tap "Manage voice models" → verify navigation
  4. Press back from Model Management → verify correct return screen

Expected:

  • All three entry points navigate to Model Management
  • Back navigation returns to the correct previous screen

11. Regression — existing download states unchanged

Steps:

  1. Install app with models already downloaded
  2. Launch app

Expected:

  • Downloaded models show Ready badge immediately
  • No unnecessary re-downloads triggered
  • isDownloaded() check prevents re-queuing
  • Bundled models (MINI_LM) show Ready badge even without download

Test results

  • :core:model-availability:test — pass
  • :core:inference:test — pass
  • :feature:settings:compileDebugKotlin — pass
  • :feature:chat:compileDebugKotlin — pass
  • All modules compile and tests pass (4 pre-existing ChatViewModelInitTest coroutine issues unrelated)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

Debug APK ready

Download app-debug.apk

Commit: 6a9819c - Build #2100

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.
@lokhor lokhor marked this pull request as ready for review June 2, 2026 10:23
@lokhor lokhor marked this pull request as draft June 2, 2026 10:30
@NickMonrad NickMonrad marked this pull request as ready for review June 3, 2026 09:30
lokhor added 4 commits June 3, 2026 19:31
…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
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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-availability with ModelAvailabilityState, mapping logic + tests, StateBadge, ModelCard/ModelCardCompact, and AvailabilitySummary.
  • Integrated the new availability UI across Settings/Chat/Voice/Model Management, including navigation entry points to Model Management.
  • Extended download plumbing with DownloadSource tracking 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
lokhor added 2 commits June 4, 2026 14:34
- 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
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 37 changed files in this pull request and generated 8 comments.

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)
}
}
lokhor added 2 commits June 4, 2026 15:39
… 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
@NickMonrad
Copy link
Copy Markdown
Owner

Delete doesn't work. Tried to delete e2b with e4b set and the button does nothing.

kernel_debug_log_0.1.0_20260604_172239_596.txt

@NickMonrad
Copy link
Copy Markdown
Owner

Deleting e4b worked even though it was selected. No other models can be deleted - the button presses but nothing happens.

lokhor added 2 commits June 4, 2026 17:29
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
@NickMonrad NickMonrad merged commit 1cfffd9 into main Jun 4, 2026
1 check passed
@NickMonrad NickMonrad deleted the feature/model-availability-ux branch June 4, 2026 12:22
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.

Epic: Model Availability UX overhaul

3 participants