From 6a421d7a849c13b5ad86c05683899231202025ca Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:42:32 +0300 Subject: [PATCH] feat(ux): add CTAs to EmptyState across screens (issue #618) (#627) Turn dead-end empty states into on-ramps by wiring the existing EmptyState actionLabel/onAction params to real next actions. Screens wired (real, existing actions): - Skills (installed): Browse skill hub -> setViewMode(HUB) - Skills (hub): Retry search -> searchHub(hubQuery) - Cron Jobs: Create your first job -> openNewJobDialog() - MCP servers: Add a server -> toggleAddForm() (literal moved to strings.xml) - Webhooks: Create subscription -> showCreateDialog() (literal moved to strings.xml) - Channels: Connect a Telegram channel -> startTelegramOnboarding() - Model: Configure model -> navigateTo(SettingsConnection) - Sessions (history): Start a new chat -> navigateTo(ChatScreen) - Analytics: Explore Skills or Cron jobs -> navigateTo(SkillsScreen) - Keys (no match): Clear search -> query = "" All new labels use strings.xml (empty_action_* / screenname_empty_*). Deviations from issue (documented honestly): - Toolsets, Profiles, Plugins keep Refresh. No in-app create path exists (toolsets are server-defined toggles; profiles/plugins have no create dialog wired in UI). Refresh is the correct recovery there, not a fake 'create' CTA. Flagged for follow-up if create flows get built. - Skills: edited the real private InstalledSkillsView in SkillsScreen.kt; the duplicate internal one in InstalledSkillsTab.kt is dead code. Verified: ktlintCheck clean, assembleDebug green. Co-authored-by: M57 Co-authored-by: Ronia --- .../ui/analytics/AnalyticsScreen.kt | 4 ++++ .../ui/channels/ChannelsScreen.kt | 2 ++ .../hermescontrol/ui/cron/CronJobsScreen.kt | 2 ++ .../m57/hermescontrol/ui/keys/KeysScreen.kt | 6 ++++-- .../hermescontrol/ui/mcp/McpServersScreen.kt | 6 ++++-- .../m57/hermescontrol/ui/model/ModelScreen.kt | 6 ++++-- .../ui/sessions/SessionsScreen.kt | 2 ++ .../hermescontrol/ui/skills/SkillsScreen.kt | 4 ++++ .../ui/skills/components/HubSkillsTab.kt | 2 ++ .../ui/webhooks/WebhooksScreen.kt | 14 +++++++++++++- app/src/main/res/values/strings.xml | 18 ++++++++++++++++++ 11 files changed, 59 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/analytics/AnalyticsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/analytics/AnalyticsScreen.kt index cc66d31f..8a4ce327 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/analytics/AnalyticsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/analytics/AnalyticsScreen.kt @@ -39,7 +39,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.m57.hermescontrol.NavigationController import com.m57.hermescontrol.R +import com.m57.hermescontrol.SkillsScreen import com.m57.hermescontrol.data.model.AnalyticsDailyEntry import com.m57.hermescontrol.data.model.ModelsAnalyticsModelEntry import com.m57.hermescontrol.theme.LocalHermesStatusColors @@ -89,6 +91,8 @@ fun AnalyticsScreen( title = stringResource(R.string.analytics_empty_title), subtitle = stringResource(R.string.analytics_empty_desc), icon = Icons.Filled.BarChart, + actionLabel = stringResource(R.string.empty_action_explore), + onAction = { NavigationController.navigateTo(SkillsScreen) }, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt index 7717f9a3..a4e46e0f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt @@ -147,6 +147,8 @@ fun ChannelsScreen( EmptyState( title = stringResource(R.string.channels_empty_title), subtitle = stringResource(R.string.channels_empty_desc), + actionLabel = stringResource(R.string.empty_action_connect_telegram), + onAction = { viewModel.startTelegramOnboarding() }, modifier = Modifier.padding(paddingValues), ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt index aca6a9e8..0e6e3f83 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt @@ -112,6 +112,8 @@ fun CronJobsScreen( icon = Icons.Filled.Schedule, title = stringResource(R.string.cron_empty_title), subtitle = stringResource(R.string.cron_empty_desc), + actionLabel = stringResource(R.string.empty_action_create_job), + onAction = { viewModel.openNewJobDialog() }, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt index 78d74c18..6270874d 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt @@ -158,8 +158,10 @@ fun KeysScreen( if (filteredCategories.isEmpty() && query.isNotBlank()) { item(key = "no-results") { EmptyState( - title = "No matching keys", - subtitle = "Try a different search term.", + title = stringResource(R.string.keys_no_matching_title), + subtitle = stringResource(R.string.keys_no_matching_desc), + actionLabel = stringResource(R.string.empty_action_clear_search), + onAction = { query = "" }, modifier = Modifier.padding(paddingValues), ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt index b76cba13..7e3357d6 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt @@ -155,8 +155,10 @@ fun McpServersScreen( if (filteredServers.isEmpty() && query.isEmpty()) { item(key = "empty") { EmptyState( - title = "No MCP servers", - subtitle = "Add a server above or from the catalog.", + title = stringResource(R.string.mcp_servers_empty_title), + subtitle = stringResource(R.string.mcp_servers_empty_desc), + actionLabel = stringResource(R.string.empty_action_add_server), + onAction = { viewModel.toggleAddForm() }, ) } } else if (filteredServers.isEmpty()) { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt index 3d6c552a..baec759c 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt @@ -54,7 +54,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.m57.hermescontrol.NavigationController import com.m57.hermescontrol.R +import com.m57.hermescontrol.SettingsConnection import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold @@ -116,8 +118,8 @@ fun ModelScreen( EmptyState( title = stringResource(R.string.model_empty_title), subtitle = stringResource(R.string.model_empty_desc), - onAction = { viewModel.loadAll() }, - actionLabel = stringResource(R.string.content_desc_refresh), + actionLabel = stringResource(R.string.empty_action_configure_model), + onAction = { NavigationController.navigateTo(SettingsConnection) }, modifier = Modifier.padding(paddingValues), ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt index ce6a47f7..2599a5d8 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt @@ -595,6 +595,8 @@ fun SessionsScreen( title = stringResource(R.string.history_empty_title), subtitle = stringResource(R.string.history_empty_desc), icon = Icons.Filled.History, + actionLabel = stringResource(R.string.empty_action_start_chat), + onAction = { NavigationController.navigateTo(ChatScreen) }, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt index 5bd1565b..b9679db5 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt @@ -150,6 +150,7 @@ fun SkillsScreen( isUninstalling = state.isUninstalling, uninstallingSkillName = state.uninstallingSkillName, onRefresh = viewModel::loadSkills, + onBrowseHub = { viewModel.setViewMode(SkillsViewMode.HUB) }, ) } @@ -219,6 +220,7 @@ private fun InstalledSkillsView( isUninstalling: Boolean, uninstallingSkillName: String?, onRefresh: () -> Unit, + onBrowseHub: () -> Unit, ) { var showDetail by remember { mutableStateOf(null) } @@ -332,6 +334,8 @@ private fun InstalledSkillsView( EmptyState( icon = Icons.Filled.Extension, title = stringResource(R.string.skills_empty_message), + actionLabel = stringResource(R.string.empty_action_browse_hub), + onAction = onBrowseHub, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/skills/components/HubSkillsTab.kt b/app/src/main/java/com/m57/hermescontrol/ui/skills/components/HubSkillsTab.kt index 66ec3063..80a67627 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/skills/components/HubSkillsTab.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/skills/components/HubSkillsTab.kt @@ -131,6 +131,8 @@ internal fun HubBrowseView( EmptyState( icon = Icons.Filled.Extension, title = stringResource(R.string.skills_hub_empty_results), + actionLabel = stringResource(R.string.empty_action_retry_search), + onAction = { onSearch(hubQuery) }, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt index 07d30840..6a6fa8e3 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt @@ -341,7 +341,7 @@ fun WebhooksScreen( EmptyState( title = if (state.subscriptions.isEmpty()) { - "No subscriptions" + stringResource(R.string.webhooks_empty_title) } else { stringResource(R.string.webhooks_no_matching) }, @@ -351,6 +351,18 @@ fun WebhooksScreen( } else { stringResource(R.string.webhooks_no_matching_desc) }, + actionLabel = + if (state.subscriptions.isEmpty()) { + stringResource(R.string.webhooks_action_add) + } else { + null + }, + onAction = + if (state.subscriptions.isEmpty()) { + { viewModel.showCreateDialog() } + } else { + null + }, ) } } else { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e4bedad..2fcc8d7e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,6 +36,19 @@ Open navigation drawer Refresh Save + + + Browse skill hub + Retry search + Create your first job + Add a server + Create subscription + Connect a Telegram channel + Configure model + Start a new chat + Install from URL + Explore Skills or Cron jobs + Clear search Cancel Clear search @@ -487,6 +500,7 @@ Global Webhooks Status Subscriptions No webhook subscriptions configured. + No subscriptions Search subscriptions… Add subscription Create @@ -645,6 +659,8 @@ Test Add server HTTP URL + No MCP servers + Add a server above or from the catalog. Stdio command Server name URL (e.g. http://localhost:8000/sse) @@ -747,6 +763,8 @@ Not configured Toggle Visibility Search keys… + No matching keys + Try a different search term. Add new key Key Value