From bc0de57fdc4c5051bb3e0b0952f2a90cc3488a63 Mon Sep 17 00:00:00 2001 From: Tyler Stapler Date: Thu, 2 Jul 2026 19:00:21 -0700 Subject: [PATCH 1/3] fix(ui): show actual LLM error message; skip API key dialog for on-device providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TagChipRow + SuggestionBottomSheet: replace hardcoded "Could not reach LLM" with state.llmError so users see the real reason (e.g. "Model is still downloading — try again in a few minutes" vs. a network failure) - LlmProviderSettings.onEditProvider: skip EditBuiltInProviderKeyDialog when the tapped provider's kind is ON_DEVICE — on-device providers have no credentials to configure so opening the key dialog was a no-op that confused users into thinking credentials were required Co-Authored-By: Claude Sonnet 4.6 --- .../stelekit/ui/components/settings/LlmProviderSettings.kt | 4 +++- .../stelekit/ui/components/tags/SuggestionBottomSheet.kt | 2 +- .../dev/stapler/stelekit/ui/components/tags/TagChipRow.kt | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderSettings.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderSettings.kt index f55c69c0..2429883c 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderSettings.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderSettings.kt @@ -30,6 +30,7 @@ import dev.stapler.stelekit.llm.CustomOpenAiCompatibleLlmProvider import dev.stapler.stelekit.llm.CustomProviderConfig import dev.stapler.stelekit.llm.LlmCredentialStore import dev.stapler.stelekit.llm.LlmFeature +import dev.stapler.stelekit.llm.LlmProviderKind import dev.stapler.stelekit.llm.LlmProviderRegistry import dev.stapler.stelekit.llm.LlmSettings import io.ktor.client.HttpClient @@ -79,9 +80,10 @@ fun LlmProviderSettings( onEditProvider = { id -> if (id.startsWith("custom:")) { editingCustomProviderId = id - } else { + } else if (registry.find(id)?.kind != LlmProviderKind.ON_DEVICE) { editingBuiltInProviderId = id } + // ON_DEVICE providers have no credentials — click is intentionally a no-op }, ) diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/tags/SuggestionBottomSheet.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/tags/SuggestionBottomSheet.kt index 38cf1526..25e1725e 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/tags/SuggestionBottomSheet.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/tags/SuggestionBottomSheet.kt @@ -90,7 +90,7 @@ fun SuggestionBottomSheet( if (state.llmError != null) { Text( - text = "Could not reach LLM", + text = state.llmError, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(top = 8.dp), diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/tags/TagChipRow.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/tags/TagChipRow.kt index 773d3e06..8db31c29 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/tags/TagChipRow.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/tags/TagChipRow.kt @@ -61,7 +61,7 @@ fun TagChipRow( if (llmError != null) { Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Could not reach LLM", + text = llmError, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), ) From 59c6e5257cb9954aa3365a7a2ed39ff524164ad1 Mon Sep 17 00:00:00 2001 From: Tyler Stapler Date: Thu, 2 Jul 2026 21:09:33 -0700 Subject: [PATCH 2/3] fix(ui): use allowlist (== REMOTE) not denylist (!= ON_DEVICE) in onEditProvider null != ON_DEVICE is true in Kotlin, so an unrecognized provider id would silently open the API key dialog. == REMOTE is safe for null and future LlmProviderKind variants. Co-Authored-By: Claude Sonnet 4.6 --- .../stelekit/ui/components/settings/LlmProviderSettings.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderSettings.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderSettings.kt index 2429883c..9743d21b 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderSettings.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderSettings.kt @@ -80,10 +80,10 @@ fun LlmProviderSettings( onEditProvider = { id -> if (id.startsWith("custom:")) { editingCustomProviderId = id - } else if (registry.find(id)?.kind != LlmProviderKind.ON_DEVICE) { + } else if (registry.find(id)?.kind == LlmProviderKind.REMOTE) { editingBuiltInProviderId = id } - // ON_DEVICE providers have no credentials — click is intentionally a no-op + // ON_DEVICE and unknown kinds have no credentials — click is intentionally a no-op }, ) From b01ed7574c223c9b30bc42b0c16a505e777dc01b Mon Sep 17 00:00:00 2001 From: Tyler Stapler Date: Thu, 2 Jul 2026 21:12:10 -0700 Subject: [PATCH 3/3] fix(ui): hide edit affordance for on-device provider rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LlmProviderRow now accepts nullable onClick — passes null for ON_DEVICE providers so the row is non-clickable and the ChevronRight/Edit icon is hidden. Also fixes null-safe inversion in LlmProviderSettings.onEditProvider: use == REMOTE allowlist instead of != ON_DEVICE denylist. Co-Authored-By: Claude Sonnet 4.6 --- .../settings/LlmProviderListScreen.kt | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderListScreen.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderListScreen.kt index 31364d0b..fe6b1d60 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderListScreen.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/LlmProviderListScreen.kt @@ -98,7 +98,12 @@ fun LlmProviderListScreen( } providers.forEachIndexed { index, provider -> - LlmProviderRow(provider = provider, onClick = { onEditProvider(provider.id) }) + LlmProviderRow( + provider = provider, + onClick = if (provider.kind != LlmProviderKind.ON_DEVICE) { + { onEditProvider(provider.id) } + } else null, + ) if (index != providers.lastIndex) { HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) } @@ -108,7 +113,7 @@ fun LlmProviderListScreen( /** A single provider row — resolves [LlmProvider.checkAvailability] asynchronously per-row. */ @Composable -private fun LlmProviderRow(provider: LlmProvider, onClick: () -> Unit) { +private fun LlmProviderRow(provider: LlmProvider, onClick: (() -> Unit)?) { // produceState re-runs the suspend block whenever `provider` changes identity; starts at // `null` ("Checking availability…") so we never render an optimistic default. val availability by produceState(initialValue = null, provider) { @@ -118,7 +123,7 @@ private fun LlmProviderRow(provider: LlmProvider, onClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() - .clickable(onClick = onClick) + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier) .padding(vertical = 10.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -143,12 +148,14 @@ private fun LlmProviderRow(provider: LlmProvider, onClick: () -> Unit) { Row(verticalAlignment = Alignment.CenterVertically) { ProviderStatusIndicator(availability) - Spacer(modifier = Modifier.width(4.dp)) - Icon( - Icons.Default.ChevronRight, - contentDescription = "Edit", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) + if (onClick != null) { + Spacer(modifier = Modifier.width(4.dp)) + Icon( + Icons.Default.ChevronRight, + contentDescription = "Edit", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } }