refactor(ui): split monolithic screen files into focused components (#485)
* refactor(ui): extract SkillsStep to components package Move the 252-LOC SkillsStep composable out of ProfilesScreen.kt into ui/profiles/components/SkillsStep.kt. Body is verbatim (behavior-preserving); only package + imports changed. Parent now imports and calls it directly. Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK). Note: sibling screens resolve Modifier.weight() without an explicit import; matching that convention (no spurious weight import) to avoid the internal RowColumnParentData.weight shadowing error. * refactor(ui): extract ProfileBuilderView cluster to components package Move the Profile Builder wizard (ProfileBuilderView + validateProfileName + IdentityStep/ModelStep/McpStep/AddMcpDialog/ReviewStep) out of ProfilesScreen.kt into ui/profiles/components/ProfileBuilderView.kt. Body is verbatim (behavior-preserving); only package + imports changed. Notable wiring required: - validateProfileName kept as internal fun: it is also used by the parent's clone dialog, so both the screen and the builder share it via import. - ProfilesUiState/ProfilesViewModel imported from the ui.profiles package. - SkillsStep call resolves to the already-extracted sibling file (same package). Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK). * refactor(ui): extract MoaConfigDialog cluster to components package Move the Mixture-of-Agents config dialog (MoaConfigDialog + MoaConfigEditor + MoaSlotPicker + MoaPickerTarget sealed class) out of ModelScreen.kt into ui/model/components/MoaConfigDialog.kt. Body is verbatim (behavior-preserving); only package + imports + entry visibility changed. - Entry MoaConfigDialog is internal (called by ModelScreen root); the editor, slot picker, and sealed class stay private to the new file. - The @Composable annotation preceding the declaration is moved with the cluster (earlier attempt left it orphaned, causing a duplicate-annotation compile error at the next declaration). Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK). * refactor(ui): extract PlatformCard + TelegramOnboardingDialog clusters Move two self-contained clusters out of ChannelsScreen.kt: - PlatformCard.kt: StateStyle (private data class) + platformStateStyle + StateBadge + PlatformCard. Entry PlatformCard is internal (called by ChannelsScreen root); StateStyle/StateBadge/platformStateStyle stay private. - TelegramOnboardingDialog.kt: TelegramOnboardingDialog. Entry internal. Wiring required: - ConfigureForm kept in ChannelsScreen as internal (shared by PlatformCard and the screen root) and imported into PlatformCard.kt. - ChannelsUiState/OnboardingPhase imported from the ui.channels package. - formatTimestamp left in the screen (used by ConfigureForm). Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK). * refactor(ui): extract SkillsScreen sub-composables to components package Split the 914-line SkillsScreen.kt into the screen root plus 4 cohesive component files under ui/skills/components/: - SourceBadge.kt: shared SourceBadge (internal, used by both tabs) - InstalledSkillsTab.kt: InstalledSkillsView + SkillCard + SkillPreviewDialog - HubSkillsTab.kt: HubBrowseView + HubSkillCard - SkillEditorDialog.kt: SkillEditorDialog Wiring required: - CATEGORY_ALL promoted private -> internal in SkillsScreen (shared by the screen root and InstalledSkillsView) and imported where needed. - SkillFilter / SkillsUiState imported from the ui.skills package. - HubBrowseView now reads isHubSearching/hubResults/hubSearchError off its SkillsUiState param (fields, not locals) - no behavior change. Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK). * refactor(ui): extract SettingsScreen + SystemScreen sub-composables SettingsScreen.kt (1238 -> ~340): move self-contained sections into ui/settings/components/ - ConnectionSection (+TestResultCard/SaveIndicator/ TestConnectionButton), BehaviorSection, AppearanceSection (+ChatSection/ NavBarSection), AboutSection, ProfileEditorDialog (+DeleteProfileConfirmDialog). Shared leaves SectionCard/InfoRow kept in the screen as internal and imported by the extracted files. SystemScreen.kt (1755 -> 1619): move CredentialEntryRow + HookCard into ui/system/components/ (both internal). formatBytes/formatDuration helpers stay in the screen (used by the root and remaining content). Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK). * fix(#485): use LocalHermesStatusColors for PlatformCard state colors Map platform connection-status badges to semantic LocalHermesStatusColors slots (success/warning/error) + add neutral slot for not_configured/disabled. Removes hardcoded hex (Color(0xFF...)) and MaterialTheme.colorScheme usage flagged by Sourcery; neutral defaults to StatusGrey so existing theme providers are untouched.
This commit is contained in:
24 files changed
+4153
-2840
No files matched your search
@@ -35,7 +35,6 @@ import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.Speed
|
||||
import androidx.compose.material.icons.filled.Stop
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material.icons.filled.Terminal
|
||||
import androidx.compose.material.icons.filled.Update
|
||||
import androidx.compose.material.icons.filled.VerifiedUser
|
||||
import androidx.compose.material3.AlertDialog
|
||||
@@ -70,8 +69,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.CredentialPoolEntry
|
||||
import com.m57.hermescontrol.data.model.HookEntry
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
import com.m57.hermescontrol.theme.LocalSpacing
|
||||
import com.m57.hermescontrol.ui.common.EmptyState
|
||||
@@ -85,6 +82,8 @@ import com.m57.hermescontrol.ui.common.StatCard
|
||||
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||
import com.m57.hermescontrol.ui.system.components.CredentialEntryRow
|
||||
import com.m57.hermescontrol.ui.system.components.HookCard
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1069,55 +1068,6 @@ private fun LazyListScope.credentialsSection(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CredentialEntryRow(
|
||||
entry: CredentialPoolEntry,
|
||||
providerName: String,
|
||||
onRemove: () -> Unit,
|
||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = spacing.xs),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = entry.label ?: "(${stringResource(R.string.system_credentials_api_key)} ${entry.index ?: 0})",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(spacing.xs)) {
|
||||
entry.token_preview?.let { preview ->
|
||||
Text(
|
||||
text = preview,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
entry.last_status?.let { status ->
|
||||
StatusBadge(
|
||||
text = status,
|
||||
status =
|
||||
when (status.lowercase()) {
|
||||
"ok", "valid", "active" -> StatusBadgeType.SUCCESS
|
||||
"error", "invalid", "expired" -> StatusBadgeType.ERROR
|
||||
else -> StatusBadgeType.NEUTRAL
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onRemove) {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = stringResource(R.string.action_delete),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.operationsSection(
|
||||
state: SystemUiState,
|
||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||
@@ -1560,97 +1510,6 @@ private fun LazyListScope.shellHooksSection(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HookCard(
|
||||
hook: HookEntry,
|
||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(spacing.md),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing.xs),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Terminal,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text(
|
||||
text = hook.command ?: "?",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(spacing.xs))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(spacing.xs)) {
|
||||
Text(
|
||||
text = hook.event ?: "",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
hook.matcher?.let { matcher ->
|
||||
Text(
|
||||
text = stringResource(R.string.system_hooks_matcher, matcher),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(spacing.xs))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(spacing.xs)) {
|
||||
StatusBadge(
|
||||
text =
|
||||
if (hook.executable != false) {
|
||||
stringResource(R.string.system_hooks_allowed)
|
||||
} else {
|
||||
stringResource(R.string.system_hooks_not_executable)
|
||||
},
|
||||
status =
|
||||
if (hook.executable != false) StatusBadgeType.SUCCESS else StatusBadgeType.ERROR,
|
||||
)
|
||||
StatusBadge(
|
||||
text =
|
||||
if (hook.allowed == true) {
|
||||
stringResource(R.string.system_hooks_allowed)
|
||||
} else {
|
||||
stringResource(R.string.system_hooks_not_approved)
|
||||
},
|
||||
status =
|
||||
if (hook.allowed == true) StatusBadgeType.SUCCESS else StatusBadgeType.WARNING,
|
||||
)
|
||||
hook.timeout?.let { t ->
|
||||
StatusBadge(
|
||||
text = "${t}s",
|
||||
status = StatusBadgeType.NEUTRAL,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = stringResource(R.string.action_delete),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.actionLogSection(
|
||||
state: SystemUiState,
|
||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.m57.hermescontrol.ui.system.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.CredentialPoolEntry
|
||||
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||
|
||||
@Composable
|
||||
internal fun CredentialEntryRow(
|
||||
entry: CredentialPoolEntry,
|
||||
providerName: String,
|
||||
onRemove: () -> Unit,
|
||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = spacing.xs),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = entry.label ?: "(${stringResource(R.string.system_credentials_api_key)} ${entry.index ?: 0})",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(spacing.xs)) {
|
||||
entry.token_preview?.let { preview ->
|
||||
Text(
|
||||
text = preview,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
entry.last_status?.let { status ->
|
||||
StatusBadge(
|
||||
text = status,
|
||||
status =
|
||||
when (status.lowercase()) {
|
||||
"ok", "valid", "active" -> StatusBadgeType.SUCCESS
|
||||
"error", "invalid", "expired" -> StatusBadgeType.ERROR
|
||||
else -> StatusBadgeType.NEUTRAL
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onRemove) {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = stringResource(R.string.action_delete),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.m57.hermescontrol.ui.system.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Terminal
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.HookEntry
|
||||
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||
|
||||
@Composable
|
||||
internal fun HookCard(
|
||||
hook: HookEntry,
|
||||
spacing: com.m57.hermescontrol.theme.Spacing,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(spacing.md),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing.xs),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Terminal,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text(
|
||||
text = hook.command ?: "?",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(spacing.xs))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(spacing.xs)) {
|
||||
Text(
|
||||
text = hook.event ?: "",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
hook.matcher?.let { matcher ->
|
||||
Text(
|
||||
text = stringResource(R.string.system_hooks_matcher, matcher),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(spacing.xs))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(spacing.xs)) {
|
||||
StatusBadge(
|
||||
text =
|
||||
if (hook.executable != false) {
|
||||
stringResource(R.string.system_hooks_allowed)
|
||||
} else {
|
||||
stringResource(R.string.system_hooks_not_executable)
|
||||
},
|
||||
status =
|
||||
if (hook.executable != false) StatusBadgeType.SUCCESS else StatusBadgeType.ERROR,
|
||||
)
|
||||
StatusBadge(
|
||||
text =
|
||||
if (hook.allowed == true) {
|
||||
stringResource(R.string.system_hooks_allowed)
|
||||
} else {
|
||||
stringResource(R.string.system_hooks_not_approved)
|
||||
},
|
||||
status =
|
||||
if (hook.allowed == true) StatusBadgeType.SUCCESS else StatusBadgeType.WARNING,
|
||||
)
|
||||
hook.timeout?.let { t ->
|
||||
StatusBadge(
|
||||
text = "${t}s",
|
||||
status = StatusBadgeType.NEUTRAL,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = stringResource(R.string.action_delete),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user