feat(#500): tap-to-reveal detail dialog for truncated item descriptions (#501)

* feat(#500): tap-to-reveal detail dialog for truncated cards

Add shared DetailDialog composable and wire tap-to-open full metadata
dialogs across Skills (installed + hub), MCP servers, Plugins, Toolsets,
and Webhooks screens. Action buttons stay on the card (sticky); tapping
the card body reveals untruncated description + key metadata.

Refs: #500

* fix(#500): resolve compile errors from missing runtime imports + dialog scope

- add missing androidx.compose.runtime.mutableStateOf/remember imports
  in InstalledSkillsTab + HubSkillsTab (caused 'Unresolved reference'
  cascade on compileDebugKotlin)
- move PluginsScreen detail dialog inside the composable function scope
  (was nested in OrphanPluginCard -> showDetail unresolved)
- DetailDialog rows type: Pair<String?,String?> -> Pair<String,String?>
  (key always non-null; fixes type-inference failure)
- drop unnecessary safe call on non-null label

Verified locally with the Android SDK:
  ./gradlew :app:compileDebugKotlin  -> BUILD SUCCESSFUL
  ./gradlew :app:testDebugUnitTest   -> BUILD SUCCESSFUL

Refs: #500

* refactor(#500): extract toDetailRows() mappers + semantic status tones; wire SkillsScreen hub browse

- add DetailRows.kt with @Composable extension mappers for Skill/HubSkill/
  McpServer/PluginInfo/Toolset/WebhookSubscription -> List<DetailRow>
- DetailDialog now renders status rows via LocalHermesStatusColors (review #2):
  SUCCESS/NEUTRAL/WARNING/ERROR/INFO tones instead of plain onSurfaceVariant
- wire SkillsScreen.HubBrowseView/HubSkillCard tap -> DetailDialog (same UX as
  other screens) — user request
- remove JVM-signature-clashing Pair overload; drop unused Color import
- add detail_dialog_trust_level string (HubSkill has no version field)
- verified: compileDebugKotlin + testDebugUnitTest PASS locally w/ ANDROID_HOME

* fix(#500): full hub description in dialog + no name truncation

- HubBrowseView (both SkillsScreen + HubSkillsTab): on card tap, fetch
  previewHubSkill(identifier) to get the FULL SkillContentResponse.content
  (search snippet was truncated); dialog shows full content once loaded,
  with a spinner while previewing. Falls back to snippet if no identifier.
- SkillsViewModel: add hubPreviewContent/isHubPreviewing/hubPreviewError
  state + previewHubSkill()/clearHubPreview() (uses existing preview endpoint)
- DetailDialog: title now wraps fully (removed maxLines=2 ellipsis cap) so
  long skill names are never cut off
- verified: compileDebugKotlin PASS + ktlint clean (local ANDROID_HOME)
This commit is contained in:
M57 authored and GitHub committed 2026-07-08 22:50:03 +03:00
1 parent 7f29cb3660
commit cbbda06157
11 files changed
+522 -13

No files matched your search

@@ -1,5 +1,6 @@
package com.m57.hermescontrol.ui.plugins
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -51,6 +52,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.model.PluginInfo
import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.ui.common.DetailDialog
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -60,6 +62,7 @@ import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.ToastEffect
import com.m57.hermescontrol.ui.common.listContentPadding
import com.m57.hermescontrol.ui.common.listItemSpacing
import com.m57.hermescontrol.ui.common.toDetailRows
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -71,6 +74,7 @@ fun PluginsScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle()
var query by remember { mutableStateOf("") }
var showDetail by remember { mutableStateOf<PluginInfo?>(null) }
val filteredPlugins =
remember(query, state.plugins) {
@@ -176,7 +180,12 @@ fun PluginsScreen(
// Plugin list
items(filteredPlugins, key = { it.name }) { plugin ->
PluginCard(plugin = plugin, state = state, viewModel = viewModel)
PluginCard(
plugin = plugin,
state = state,
viewModel = viewModel,
onClick = { showDetail = plugin },
)
}
// Orphan dashboard plugins section
@@ -191,7 +200,7 @@ fun PluginsScreen(
)
}
items(state.orphanPlugins, key = { "orphan-${it.name}" }) { plugin ->
OrphanPluginCard(plugin = plugin)
OrphanPluginCard(plugin = plugin, onClick = { showDetail = plugin })
}
}
}
@@ -199,6 +208,14 @@ fun PluginsScreen(
}
}
}
showDetail?.let { plugin ->
DetailDialog(
title = plugin.name,
rows = plugin.toDetailRows(),
onDismiss = { showDetail = null },
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -422,11 +439,12 @@ private fun PluginCard(
plugin: PluginInfo,
state: PluginsUiState,
viewModel: PluginsViewModel,
onClick: () -> Unit,
) {
val busy = state.rowBusy == plugin.name
val statusColors = LocalHermesStatusColors.current
Card(modifier = Modifier.fillMaxWidth()) {
Card(modifier = Modifier.fillMaxWidth().clickable(onClick = onClick)) {
Column(modifier = Modifier.padding(16.dp)) {
// Header row: name + toggle
Row(
@@ -588,8 +606,11 @@ private fun PluginCard(
}
@Composable
private fun OrphanPluginCard(plugin: PluginInfo) {
Card(modifier = Modifier.fillMaxWidth()) {
private fun OrphanPluginCard(
plugin: PluginInfo,
onClick: () -> Unit,
) {
Card(modifier = Modifier.fillMaxWidth().clickable(onClick = onClick)) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,