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 <hy4ri@users.noreply.github.com>
Co-authored-by: Ronia <ronia@hermes>
This commit is contained in:
authored and GitHub committed 2026-07-18 16:42:32 +03:00
1 parent 9661246497
commit 6a421d7a84
11 files changed
+59 -7

No files matched your search

@@ -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) },
)
}
@@ -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),
)
}
@@ -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() },
)
}
@@ -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),
)
}
@@ -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()) {
@@ -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),
)
}
@@ -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) },
)
}
@@ -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<Skill?>(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,
)
}
@@ -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) },
)
}
@@ -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 {